From e2ec4adcbf954505f84dea6436af380e2bc0af72 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Thu, 25 Aug 2016 17:40:05 +0200 Subject: [PATCH 001/213] Initial commit --- Makefile | 9 +++++++++ rootfs/Dockerfile | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 Makefile create mode 100644 rootfs/Dockerfile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..903797e --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +VERSION := $(shell git describe --tags --exact-match 2>/dev/null || echo latest) +DOCKERHUB_NAMESPACE ?= keitaro +IMAGE := ${DOCKERHUB_NAMESPACE}/ckan:${VERSION} + +build: + docker build -t ${IMAGE} rootfs + +push: build + docker push ${IMAGE} diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile new file mode 100644 index 0000000..d4537fb --- /dev/null +++ b/rootfs/Dockerfile @@ -0,0 +1,51 @@ +FROM keitaro/base:0.1 + +MAINTAINER Keitaro Inc + +ENV APP_DIR=/srv/app +ENV GIT_URL=https://github.com/ckan/ckan.git +ENV GIT_BRANCH=release-v2.5.2 +ENV APP_WSGI=ckan.wsgi +ENV CKAN_SITE_URL=http://localhost +ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars + +WORKDIR ${APP_DIR} + +# Install necessary packages to run CKAN +RUN apk add --no-cache git \ + gettext \ + postgresql-client \ + postgresql-dev \ + python \ + py-pip \ + py-gunicorn + +# Temporary packages to build CKAN requirements +RUN apk add --no-cache --virtual .build-deps \ + gcc \ + musl-dev \ + python-dev + +# Fetch CKAN and install +RUN mkdir ${APP_DIR}/src && cd ${APP_DIR}/src && \ + git clone -b ${GIT_BRANCH} --depth=1 --single-branch ${GIT_URL} ckan && \ + cd ckan && \ + cp who.ini ${APP_DIR} && \ + python setup.py install && \ + pip install --no-cache-dir testrepository && \ + pip install --no-cache-dir --upgrade -r requirements.txt && \ + pip install --no-cache-dir html5lib==0.9999999 + +# Remove temporary packages +RUN apk del .build-deps + +# Default Extensions +RUN pip install --no-cache-dir git+https://github.com/okfn/ckanext-envvars.git#egg=ckanext-envvars + +# Create and update CKAN config +RUN paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini +RUN paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" + +EXPOSE 8000 + +CMD ["gunicorn", "--paste", "production.ini"] From de7eaa90d5ae499a302349217172aa62bdfb5ff4 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Thu, 25 Aug 2016 21:46:38 +0200 Subject: [PATCH 002/213] Minor update --- rootfs/Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index d4537fb..ec56b03 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -5,7 +5,6 @@ MAINTAINER Keitaro Inc ENV APP_DIR=/srv/app ENV GIT_URL=https://github.com/ckan/ckan.git ENV GIT_BRANCH=release-v2.5.2 -ENV APP_WSGI=ckan.wsgi ENV CKAN_SITE_URL=http://localhost ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars @@ -14,14 +13,14 @@ WORKDIR ${APP_DIR} # Install necessary packages to run CKAN RUN apk add --no-cache git \ gettext \ - postgresql-client \ - postgresql-dev \ python \ py-pip \ py-gunicorn # Temporary packages to build CKAN requirements RUN apk add --no-cache --virtual .build-deps \ + postgresql-client \ + postgresql-dev \ gcc \ musl-dev \ python-dev @@ -36,8 +35,9 @@ RUN mkdir ${APP_DIR}/src && cd ${APP_DIR}/src && \ pip install --no-cache-dir --upgrade -r requirements.txt && \ pip install --no-cache-dir html5lib==0.9999999 -# Remove temporary packages -RUN apk del .build-deps +# Remove temporary packages and files +RUN apk del .build-deps && \ + rm -rf ${APP_DIR}/src # Default Extensions RUN pip install --no-cache-dir git+https://github.com/okfn/ckanext-envvars.git#egg=ckanext-envvars From acb5913a643ae898f59d466902c6213834a08049 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Thu, 25 Aug 2016 22:05:30 +0200 Subject: [PATCH 003/213] Add pre-run --- rootfs/Dockerfile | 35 +++++++++--------- rootfs/setup/prerun.py | 72 ++++++++++++++++++++++++++++++++++++++ rootfs/setup/start_ckan.sh | 3 ++ 3 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 rootfs/setup/prerun.py create mode 100755 rootfs/setup/start_ckan.sh diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index ec56b03..1697ac5 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -13,39 +13,38 @@ WORKDIR ${APP_DIR} # Install necessary packages to run CKAN RUN apk add --no-cache git \ gettext \ + postgresql-client \ python \ py-pip \ - py-gunicorn - -# Temporary packages to build CKAN requirements -RUN apk add --no-cache --virtual .build-deps \ - postgresql-client \ + py-gunicorn && \ + # Temporary packages to build CKAN requirements + apk add --no-cache --virtual .build-deps \ postgresql-dev \ gcc \ musl-dev \ - python-dev - -# Fetch CKAN and install -RUN mkdir ${APP_DIR}/src && cd ${APP_DIR}/src && \ + python-dev && \ + # Fetch CKAN and install + mkdir ${APP_DIR}/src && cd ${APP_DIR}/src && \ git clone -b ${GIT_BRANCH} --depth=1 --single-branch ${GIT_URL} ckan && \ cd ckan && \ cp who.ini ${APP_DIR} && \ python setup.py install && \ pip install --no-cache-dir testrepository && \ pip install --no-cache-dir --upgrade -r requirements.txt && \ - pip install --no-cache-dir html5lib==0.9999999 - -# Remove temporary packages and files -RUN apk del .build-deps && \ + # Fix issue with html5lib in 2.5.2 + pip install --no-cache-dir html5lib==0.9999999 && \ + # Remove temporary packages and files + apk del .build-deps && \ rm -rf ${APP_DIR}/src # Default Extensions -RUN pip install --no-cache-dir git+https://github.com/okfn/ckanext-envvars.git#egg=ckanext-envvars +RUN pip install --no-cache-dir git+https://github.com/okfn/ckanext-envvars.git#egg=ckanext-envvars && \ + # Create and update CKAN config + paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ + paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" -# Create and update CKAN config -RUN paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini -RUN paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" +COPY setup ${APP_DIR} EXPOSE 8000 -CMD ["gunicorn", "--paste", "production.ini"] +CMD ["/srv/app/start_ckan.sh"] diff --git a/rootfs/setup/prerun.py b/rootfs/setup/prerun.py new file mode 100644 index 0000000..3552b15 --- /dev/null +++ b/rootfs/setup/prerun.py @@ -0,0 +1,72 @@ +import os +import sys +import subprocess + + +ckan_ini = os.environ.get('CKAN_INI', '') + + +def init_db(): + + db_command = ['paster', '--plugin=ckan', 'db', + 'init', '-c', ckan_ini] + print '[prerun] Initializing or upgrading db - start' + try: + subprocess.check_output(db_command, stderr=subprocess.STDOUT) + print '[prerun] Initializing or upgrading db - end' + except subprocess.CalledProcessError, e: + if 'OperationalError' in e.output: + print e.output + print '[prerun] Database not ready, waiting a bit before exit...' + import time + time.sleep(5) + sys.exit(1) + else: + print e.output + raise e + + +def create_sysadmin(): + + name = os.environ.get('CKAN_SYSADMIN_NAME') + password = os.environ.get('CKAN_SYSADMIN_PASSWORD') + email = os.environ.get('CKAN_SYSADMIN_EMAIL') + + if name and password and email: + + # Check if user exists + command = ['paster', '--plugin=ckan', 'user', name, '-c', ckan_ini] + + out = subprocess.check_output(command) + if 'User: \nNone\n' not in out: + print '[prerun] Sysadmin user exists, skipping creation' + return + + # Create user + command = ['paster', '--plugin=ckan', 'user', 'add', + name, + 'password=' + password, + 'email=' + email, + '-c', ckan_ini] + + subprocess.call(command) + print '[prerun] Created user {0}'.format(name) + + # Make it sysadmin + command = ['paster', '--plugin=ckan', 'sysadmin', 'add', + name, + '-c', ckan_ini] + + subprocess.call(command) + print '[prerun] Made user {0} a sysadmin'.format(name) + + +if __name__ == '__main__': + + maintenance = os.environ.get('MAINTENANCE_MODE', '').lower() == 'true' + + if maintenance: + print '[prerun] Maintenance mode, skipping setup...' + else: + init_db() + create_sysadmin() diff --git a/rootfs/setup/start_ckan.sh b/rootfs/setup/start_ckan.sh new file mode 100755 index 0000000..209d664 --- /dev/null +++ b/rootfs/setup/start_ckan.sh @@ -0,0 +1,3 @@ +#!/bin/bash +python prerun.py +gunicorn --log-file=- --paste production.ini From ef088d177ad5fae0f378bf0cb733cf6c2e7cc068 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Thu, 25 Aug 2016 22:10:24 +0200 Subject: [PATCH 004/213] Set default value for APP INI --- rootfs/setup/prerun.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rootfs/setup/prerun.py b/rootfs/setup/prerun.py index 3552b15..7b1eb16 100644 --- a/rootfs/setup/prerun.py +++ b/rootfs/setup/prerun.py @@ -3,7 +3,7 @@ import sys import subprocess -ckan_ini = os.environ.get('CKAN_INI', '') +ckan_ini = os.environ.get('CKAN_INI', '/srv/app/production.ini') def init_db(): From 4847ee0e20a7f0e440d74b3fdb5854e72f8db99d Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Thu, 25 Aug 2016 22:13:02 +0200 Subject: [PATCH 005/213] Update port to 5000 --- rootfs/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 1697ac5..c9a3b53 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -45,6 +45,6 @@ RUN pip install --no-cache-dir git+https://github.com/okfn/ckanext-envvars.git#e COPY setup ${APP_DIR} -EXPOSE 8000 +EXPOSE 5000 CMD ["/srv/app/start_ckan.sh"] From 9504ee4b2759370a865572bc723ef63447e5d6b4 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Thu, 25 Aug 2016 23:04:32 +0200 Subject: [PATCH 006/213] 2.5.2 map-fix --- rootfs/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index c9a3b53..52200f5 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -3,9 +3,9 @@ FROM keitaro/base:0.1 MAINTAINER Keitaro Inc ENV APP_DIR=/srv/app -ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=release-v2.5.2 -ENV CKAN_SITE_URL=http://localhost +ENV GIT_URL=https://github.com/keitaroinc/ckan.git +ENV GIT_BRANCH=ckan-2.5-map-fix3 +ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars WORKDIR ${APP_DIR} From 2feab6e9c60a80415d87cf25e8b19298b1adea47 Mon Sep 17 00:00:00 2001 From: Petar Efnushev Date: Fri, 26 Aug 2016 10:30:53 +0200 Subject: [PATCH 007/213] Add prerun checks for postgres & solr database --- rootfs/setup/prerun.py | 49 ++++++++++++++++++++++++++++++++++++++ rootfs/setup/start_ckan.sh | 7 +++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/rootfs/setup/prerun.py b/rootfs/setup/prerun.py index 7b1eb16..842763a 100644 --- a/rootfs/setup/prerun.py +++ b/rootfs/setup/prerun.py @@ -1,10 +1,57 @@ import os import sys import subprocess +import psycopg2 +import urllib2 ckan_ini = os.environ.get('CKAN_INI', '/srv/app/production.ini') +RETRY = 5 + +def check_db_connection(retry=None): + + if retry is None: + retry = RETRY + elif retry == 0: + print '[prerun] Giving up after 5 tries...' + sys.exit(1) + + conn_str = os.environ.get('CKAN_SQLALCHEMY_URL', '') + try: + connection = psycopg2.connect(conn_str) + + except psycopg2.Error as e: + print str(e) + print '[prerun] Unable to connect to the database...try again in a while.' + import time + time.sleep(10) + check_db_connection(retry = retry - 1) + else: + connection.close() + +def check_solr_connection(retry=None): + + if retry is None: + retry = RETRY + elif retry == 0: + print '[prerun] Giving up after 5 tries...' + sys.exit(1) + + url = os.environ.get('CKAN_SOLR_URL', '') + search_url = '{url}/select/?q=*&wt=json'.format(url=url) + + try: + connection = urllib2.urlopen(search_url) + except urllib2.URLError as e: + print str(e) + print '[prerun] Unable to connect to solr...try again in a while.' + import time + time.sleep(10) + check_solr_connection(retry = retry - 1) + else: + eval(connection.read()) + def init_db(): @@ -68,5 +115,7 @@ if __name__ == '__main__': if maintenance: print '[prerun] Maintenance mode, skipping setup...' else: + check_db_connection() + check_solr_connection() init_db() create_sysadmin() diff --git a/rootfs/setup/start_ckan.sh b/rootfs/setup/start_ckan.sh index 209d664..dd8a8da 100755 --- a/rootfs/setup/start_ckan.sh +++ b/rootfs/setup/start_ckan.sh @@ -1,3 +1,8 @@ #!/bin/bash python prerun.py -gunicorn --log-file=- --paste production.ini +if [ $? -eq 0 ] +then + gunicorn --log-file=- --paste production.ini +else + echo "[prerun] failed...not starting CKAN." +fi From d576febe7469a45a36cf67f06dc6a67c8ba9ceef Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 26 Aug 2016 10:44:49 +0200 Subject: [PATCH 008/213] Remove white spaces --- rootfs/setup/prerun.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rootfs/setup/prerun.py b/rootfs/setup/prerun.py index 842763a..21dc9b1 100644 --- a/rootfs/setup/prerun.py +++ b/rootfs/setup/prerun.py @@ -10,17 +10,17 @@ ckan_ini = os.environ.get('CKAN_INI', '/srv/app/production.ini') RETRY = 5 def check_db_connection(retry=None): - + if retry is None: retry = RETRY elif retry == 0: print '[prerun] Giving up after 5 tries...' sys.exit(1) - + conn_str = os.environ.get('CKAN_SQLALCHEMY_URL', '') try: connection = psycopg2.connect(conn_str) - + except psycopg2.Error as e: print str(e) print '[prerun] Unable to connect to the database...try again in a while.' @@ -29,18 +29,18 @@ def check_db_connection(retry=None): check_db_connection(retry = retry - 1) else: connection.close() - + def check_solr_connection(retry=None): - + if retry is None: retry = RETRY elif retry == 0: print '[prerun] Giving up after 5 tries...' sys.exit(1) - + url = os.environ.get('CKAN_SOLR_URL', '') search_url = '{url}/select/?q=*&wt=json'.format(url=url) - + try: connection = urllib2.urlopen(search_url) except urllib2.URLError as e: From cc1f4c884c916b43a5f9150430f1533da60c7dee Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 26 Oct 2016 09:28:54 +0200 Subject: [PATCH 009/213] Add geos library --- rootfs/Dockerfile | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 52200f5..9cf324b 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -3,6 +3,7 @@ FROM keitaro/base:0.1 MAINTAINER Keitaro Inc ENV APP_DIR=/srv/app +ENV SRC_DIR=/srv/app/src ENV GIT_URL=https://github.com/keitaroinc/ckan.git ENV GIT_BRANCH=ckan-2.5-map-fix3 ENV CKAN_SITE_URL=http://localhost:5000 @@ -12,21 +13,32 @@ WORKDIR ${APP_DIR} # Install necessary packages to run CKAN RUN apk add --no-cache git \ - gettext \ - postgresql-client \ - python \ - py-pip \ - py-gunicorn && \ + gettext \ + postgresql-client \ + python \ + py-pip \ + py-gunicorn && \ # Temporary packages to build CKAN requirements apk add --no-cache --virtual .build-deps \ - postgresql-dev \ - gcc \ - musl-dev \ - python-dev && \ + postgresql-dev \ + gcc \ + make \ + g++ \ + autoconf \ + automake \ + libtool \ + musl-dev \ + python-dev && \ + # Build and install libgeos to support geospatial + git clone -b 3.6.0 --depth=1 --single-branch https://git.osgeo.org/gogs/geos/geos.git ${SRC_DIR}/geos && \ + cd ${SRC_DIR}/geos && \ + ./autogen.sh && \ + ./configure && \ + make -j2 && \ + make install && \ # Fetch CKAN and install - mkdir ${APP_DIR}/src && cd ${APP_DIR}/src && \ - git clone -b ${GIT_BRANCH} --depth=1 --single-branch ${GIT_URL} ckan && \ - cd ckan && \ + git clone -b ${GIT_BRANCH} --depth=1 --single-branch ${GIT_URL} ${SRC_DIR}/ckan && \ + cd ${SRC_DIR}/ckan && \ cp who.ini ${APP_DIR} && \ python setup.py install && \ pip install --no-cache-dir testrepository && \ @@ -35,7 +47,7 @@ RUN apk add --no-cache git \ pip install --no-cache-dir html5lib==0.9999999 && \ # Remove temporary packages and files apk del .build-deps && \ - rm -rf ${APP_DIR}/src + rm -rf ${SRC_DIR} # Default Extensions RUN pip install --no-cache-dir git+https://github.com/okfn/ckanext-envvars.git#egg=ckanext-envvars && \ @@ -43,6 +55,9 @@ RUN pip install --no-cache-dir git+https://github.com/okfn/ckanext-envvars.git#e paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" +# CKAN plugins to enable +ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars + COPY setup ${APP_DIR} EXPOSE 5000 From d2301f763358c3b4116366aa83d6243f02b775b4 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 26 Oct 2016 09:35:52 +0200 Subject: [PATCH 010/213] Add pages extension to default image --- rootfs/Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 9cf324b..5479efb 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -49,15 +49,16 @@ RUN apk add --no-cache git \ apk del .build-deps && \ rm -rf ${SRC_DIR} +# CKAN plugins to enable +ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher pages envvars + # Default Extensions RUN pip install --no-cache-dir git+https://github.com/okfn/ckanext-envvars.git#egg=ckanext-envvars && \ + pip install --no-cache-dir git+https://github.com/ckan/ckanext-pages.git#egg=ckanext-pages && \ # Create and update CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" -# CKAN plugins to enable -ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars - COPY setup ${APP_DIR} EXPOSE 5000 From 87226ae7541531172320d242c977405f11db00a3 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Mon, 7 Nov 2016 12:10:47 +0100 Subject: [PATCH 011/213] Update to 2.5.3 --- rootfs/Dockerfile | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 5479efb..4b2c91c 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -4,8 +4,8 @@ MAINTAINER Keitaro Inc ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src -ENV GIT_URL=https://github.com/keitaroinc/ckan.git -ENV GIT_BRANCH=ckan-2.5-map-fix3 +ENV GIT_URL=https://github.com/ckan/ckan.git +ENV GIT_BRANCH=ckan-2.5.3 ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars @@ -33,7 +33,7 @@ RUN apk add --no-cache git \ git clone -b 3.6.0 --depth=1 --single-branch https://git.osgeo.org/gogs/geos/geos.git ${SRC_DIR}/geos && \ cd ${SRC_DIR}/geos && \ ./autogen.sh && \ - ./configure && \ + ./configure --prefix /usr && \ make -j2 && \ make install && \ # Fetch CKAN and install @@ -43,8 +43,6 @@ RUN apk add --no-cache git \ python setup.py install && \ pip install --no-cache-dir testrepository && \ pip install --no-cache-dir --upgrade -r requirements.txt && \ - # Fix issue with html5lib in 2.5.2 - pip install --no-cache-dir html5lib==0.9999999 && \ # Remove temporary packages and files apk del .build-deps && \ rm -rf ${SRC_DIR} From c83b8bebf3dbefb45a7cf9d87e017ad36b495994 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 27 Jan 2017 10:51:02 +0100 Subject: [PATCH 012/213] Upgrade to CKAN 2.6.0 --- rootfs/Dockerfile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 4b2c91c..5e25e4f 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -5,7 +5,7 @@ MAINTAINER Keitaro Inc ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.5.3 +ENV GIT_BRANCH=ckan-2.6.0 ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars @@ -17,7 +17,7 @@ RUN apk add --no-cache git \ postgresql-client \ python \ py-pip \ - py-gunicorn && \ + py-gunicorn && \ # Temporary packages to build CKAN requirements apk add --no-cache --virtual .build-deps \ postgresql-dev \ @@ -48,11 +48,10 @@ RUN apk add --no-cache git \ rm -rf ${SRC_DIR} # CKAN plugins to enable -ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher pages envvars +ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars # Default Extensions RUN pip install --no-cache-dir git+https://github.com/okfn/ckanext-envvars.git#egg=ckanext-envvars && \ - pip install --no-cache-dir git+https://github.com/ckan/ckanext-pages.git#egg=ckanext-pages && \ # Create and update CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" From 12f574bc1876df89134493f03098e845ee18e0de Mon Sep 17 00:00:00 2001 From: Petar Efnushev Date: Tue, 4 Apr 2017 09:04:18 +0200 Subject: [PATCH 013/213] Add dependency for gevent --- rootfs/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 5e25e4f..e381686 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -17,6 +17,7 @@ RUN apk add --no-cache git \ postgresql-client \ python \ py-pip \ + py-gevent \ py-gunicorn && \ # Temporary packages to build CKAN requirements apk add --no-cache --virtual .build-deps \ @@ -43,6 +44,7 @@ RUN apk add --no-cache git \ python setup.py install && \ pip install --no-cache-dir testrepository && \ pip install --no-cache-dir --upgrade -r requirements.txt && \ + pip install --no-cache-dir gevent && \ # Remove temporary packages and files apk del .build-deps && \ rm -rf ${SRC_DIR} From 4cdecebe6e4133532281591501e22dbf30c206d7 Mon Sep 17 00:00:00 2001 From: Petar Efnushev Date: Tue, 4 Apr 2017 09:05:13 +0200 Subject: [PATCH 014/213] Start CKAN using gevent for concurrency --- rootfs/setup/start_ckan.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rootfs/setup/start_ckan.sh b/rootfs/setup/start_ckan.sh index dd8a8da..bac73a4 100755 --- a/rootfs/setup/start_ckan.sh +++ b/rootfs/setup/start_ckan.sh @@ -2,7 +2,7 @@ python prerun.py if [ $? -eq 0 ] then - gunicorn --log-file=- --paste production.ini + gunicorn --log-file=- -k gevent -w 4 --paste production.ini else echo "[prerun] failed...not starting CKAN." fi From 0ebeaad07675d59dc7a16c747eed6d0648b711ee Mon Sep 17 00:00:00 2001 From: Jovanka Gulicoska Date: Tue, 4 Apr 2017 11:07:13 +0200 Subject: [PATCH 015/213] Remove py-gevent from apk add --- rootfs/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index e381686..9a7ca7e 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -17,7 +17,6 @@ RUN apk add --no-cache git \ postgresql-client \ python \ py-pip \ - py-gevent \ py-gunicorn && \ # Temporary packages to build CKAN requirements apk add --no-cache --virtual .build-deps \ From db0b33437d1d85f2f46529aac0f37fe7b71282c1 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Thu, 6 Apr 2017 11:25:21 +0200 Subject: [PATCH 016/213] Change docker layout and setup install for CKAN 2.6.2 with password protect option --- rootfs/Dockerfile | 54 +++++++++++++++++++++++--------------- rootfs/setup/nginx.conf | 28 ++++++++++++++++++++ rootfs/setup/start_ckan.sh | 16 ++++++++++- 3 files changed, 76 insertions(+), 22 deletions(-) create mode 100644 rootfs/setup/nginx.conf diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 9a7ca7e..562a78e 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -4,21 +4,25 @@ MAINTAINER Keitaro Inc ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.6.0 +ENV GIT_BRANCH=ckan-2.6.2 ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars WORKDIR ${APP_DIR} +######################### +### Base docker layer ### +######################### # Install necessary packages to run CKAN RUN apk add --no-cache git \ gettext \ postgresql-client \ python \ - py-pip \ - py-gunicorn && \ - # Temporary packages to build CKAN requirements + nginx \ + apache2-utils && \ + # Packages to build CKAN requirements and plugins apk add --no-cache --virtual .build-deps \ postgresql-dev \ gcc \ @@ -29,30 +33,38 @@ RUN apk add --no-cache git \ libtool \ musl-dev \ python-dev && \ - # Build and install libgeos to support geospatial - git clone -b 3.6.0 --depth=1 --single-branch https://git.osgeo.org/gogs/geos/geos.git ${SRC_DIR}/geos && \ + # Create SRC_DIR + mkdir -p ${SRC_DIR} && \ + # Create nginx run dir + mkdir -p /run/nginx && \ + # Install pip and gunicorn + curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py && \ + pip install gunicorn gevent && \ + rm -rf ${SRC_DIR}/get-pip.py + +############################ +### libgeos docker layer ### +############################ +# Build and install libgeos to support geospatial +RUN git clone -b 3.6.0 --depth=1 --single-branch https://git.osgeo.org/gogs/geos/geos.git ${SRC_DIR}/geos && \ cd ${SRC_DIR}/geos && \ ./autogen.sh && \ ./configure --prefix /usr && \ make -j2 && \ make install && \ - # Fetch CKAN and install - git clone -b ${GIT_BRANCH} --depth=1 --single-branch ${GIT_URL} ${SRC_DIR}/ckan && \ + rm -rf ${SRC_DIR}/geos + +######################### +### CKAN docker layer ### +######################### +# Install CKAN +RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan && \ cd ${SRC_DIR}/ckan && \ cp who.ini ${APP_DIR} && \ - python setup.py install && \ - pip install --no-cache-dir testrepository && \ - pip install --no-cache-dir --upgrade -r requirements.txt && \ - pip install --no-cache-dir gevent && \ - # Remove temporary packages and files - apk del .build-deps && \ - rm -rf ${SRC_DIR} - -# CKAN plugins to enable -ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars - -# Default Extensions -RUN pip install --no-cache-dir git+https://github.com/okfn/ckanext-envvars.git#egg=ckanext-envvars && \ + pip install -r requirements.txt && \ + # Install CKAN envvars to support loading config from environment variables + pip install -e git+https://github.com/okfn/ckanext-envvars.git#egg=ckanext-envvars && \ # Create and update CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" diff --git a/rootfs/setup/nginx.conf b/rootfs/setup/nginx.conf new file mode 100644 index 0000000..fd2bf23 --- /dev/null +++ b/rootfs/setup/nginx.conf @@ -0,0 +1,28 @@ +worker_processes 4; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + sendfile on; + client_max_body_size 100m; + keepalive_timeout 65; + + server { + listen 5000; + server_name localhost; + + auth_basic "Restricted"; + auth_basic_user_file /srv/app/.htpasswd; + + location / { + auth_basic "Restricted"; + auth_basic_user_file /srv/app/.htpasswd; + proxy_pass http://127.0.0.1:4000; + } + } +} diff --git a/rootfs/setup/start_ckan.sh b/rootfs/setup/start_ckan.sh index bac73a4..4c76d16 100755 --- a/rootfs/setup/start_ckan.sh +++ b/rootfs/setup/start_ckan.sh @@ -2,7 +2,21 @@ python prerun.py if [ $? -eq 0 ] then - gunicorn --log-file=- -k gevent -w 4 --paste production.ini + if [ "$PASSWORD_PROTECT" = true ] + then + if [ "$HTPASSWD_USER" ] || [ "$HTPASSWD_PASSWORD" ] + then + cp -a /srv/app/nginx.conf /etc/nginx/nginx.conf + htpasswd -b -c /srv/app/.htpasswd $HTPASSWD_USER $HTPASSWD_PASSWORD + nginx + gunicorn --log-file=- -k gevent -w 4 -b 127.0.0.1:4000 --paste production.ini + else + echo "Missing HTPASSWD_USER or HTPASSWD_PASSWORD environment variables. Exiting..." + exit 1 + fi + else + gunicorn --log-file=- -k gevent -w 4 -b 0.0.0.0:5000 --paste production.ini + fi else echo "[prerun] failed...not starting CKAN." fi From 353c88d4738a6a5bec26a6050e0e3bae682627b5 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 7 Apr 2017 09:10:07 +0200 Subject: [PATCH 017/213] Pass host and x-forwarded-for headers when using nginx reverse proxy --- rootfs/setup/nginx.conf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rootfs/setup/nginx.conf b/rootfs/setup/nginx.conf index fd2bf23..1811bc4 100644 --- a/rootfs/setup/nginx.conf +++ b/rootfs/setup/nginx.conf @@ -22,7 +22,9 @@ http { location / { auth_basic "Restricted"; auth_basic_user_file /srv/app/.htpasswd; - proxy_pass http://127.0.0.1:4000; + proxy_pass http://127.0.0.1:4000/; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $host; } } } From fa5a3603ba108e2f8b3ae41363c16c149b2f4257 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 7 Apr 2017 09:34:40 +0200 Subject: [PATCH 018/213] Add health check to docker image --- rootfs/Dockerfile | 2 ++ rootfs/setup/nginx.conf | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 562a78e..def0b6a 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -73,4 +73,6 @@ COPY setup ${APP_DIR} EXPOSE 5000 +HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 + CMD ["/srv/app/start_ckan.sh"] diff --git a/rootfs/setup/nginx.conf b/rootfs/setup/nginx.conf index 1811bc4..ca21636 100644 --- a/rootfs/setup/nginx.conf +++ b/rootfs/setup/nginx.conf @@ -23,6 +23,17 @@ http { auth_basic "Restricted"; auth_basic_user_file /srv/app/.htpasswd; proxy_pass http://127.0.0.1:4000/; + proxy_redirect off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $host; + } + + location /api/3/action/status_show { + auth_basic "off"; + proxy_pass http://127.0.0.1:4000/api/3/action/status_show; + proxy_redirect off; + proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Host $host; } From 237ea992f2597c036ec39787dfdf2c955345cb86 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Tue, 9 May 2017 18:22:04 +0200 Subject: [PATCH 019/213] Migrate from gunicorn to uwsgi and remove nginx dependency for http basicauth --- rootfs/Dockerfile | 28 ++++++++++++++-------- rootfs/setup/{ => app}/prerun.py | 0 rootfs/setup/app/start_ckan.sh | 29 ++++++++++++++++++++++ rootfs/setup/app/uwsgi.conf | 2 ++ rootfs/setup/nginx.conf | 41 -------------------------------- rootfs/setup/start_ckan.sh | 22 ----------------- 6 files changed, 49 insertions(+), 73 deletions(-) rename rootfs/setup/{ => app}/prerun.py (100%) create mode 100755 rootfs/setup/app/start_ckan.sh create mode 100644 rootfs/setup/app/uwsgi.conf delete mode 100644 rootfs/setup/nginx.conf delete mode 100755 rootfs/setup/start_ckan.sh diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index def0b6a..f9073e7 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -1,6 +1,6 @@ -FROM keitaro/base:0.1 +FROM keitaro/base:0.2 -MAINTAINER Keitaro Inc +MAINTAINER Keitaro Inc ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src @@ -15,16 +15,19 @@ WORKDIR ${APP_DIR} ######################### ### Base docker layer ### ######################### +# Create a local user and group to run the app +RUN addgroup -g 92 -S ckan && \ + adduser -u 92 -h /srv/app -H -D -S -G ckan ckan # Install necessary packages to run CKAN RUN apk add --no-cache git \ gettext \ postgresql-client \ python \ - nginx \ apache2-utils && \ # Packages to build CKAN requirements and plugins apk add --no-cache --virtual .build-deps \ postgresql-dev \ + linux-headers \ gcc \ make \ g++ \ @@ -32,15 +35,14 @@ RUN apk add --no-cache git \ automake \ libtool \ musl-dev \ + pcre-dev \ python-dev && \ # Create SRC_DIR mkdir -p ${SRC_DIR} && \ - # Create nginx run dir - mkdir -p /run/nginx && \ - # Install pip and gunicorn + # Install pip and uwsgi curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ python ${SRC_DIR}/get-pip.py && \ - pip install gunicorn gevent && \ + pip install --no-cache-dir uwsgi gevent && \ rm -rf ${SRC_DIR}/get-pip.py ############################ @@ -64,15 +66,21 @@ RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan && \ cp who.ini ${APP_DIR} && \ pip install -r requirements.txt && \ # Install CKAN envvars to support loading config from environment variables - pip install -e git+https://github.com/okfn/ckanext-envvars.git#egg=ckanext-envvars && \ + pip install -e git+https://github.com/okfn/ckanext-envvars.git@0.0.1#egg=ckanext-envvars && \ # Create and update CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ - paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" + paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + # Change ownership to app user + chown -R ckan:ckan /srv/app && \ + # Clear the .git directory + rm -rf /srv/app/src/ckan/.git -COPY setup ${APP_DIR} +COPY setup/app ${APP_DIR} EXPOSE 5000 HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 +USER ckan + CMD ["/srv/app/start_ckan.sh"] diff --git a/rootfs/setup/prerun.py b/rootfs/setup/app/prerun.py similarity index 100% rename from rootfs/setup/prerun.py rename to rootfs/setup/app/prerun.py diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh new file mode 100755 index 0000000..d9fb405 --- /dev/null +++ b/rootfs/setup/app/start_ckan.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Run the prerun script to init CKAN and create the default admin user +python prerun.py + +# Set the common uwsgi options +UWSGI_OPTS="--socket /tmp/uwsgi.sock --thunder-lock --uid 92 --gid 92 --http :5000 --master --single-interpreter --enable-threads --paste config:/srv/app/production.ini --gevent 2000 -p 4 -L" + +# Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully +if [ $? -eq 0 ] +then + if [ "$PASSWORD_PROTECT" = true ] + then + if [ "$HTPASSWD_USER" ] || [ "$HTPASSWD_PASSWORD" ] + then + # Generate htpasswd file for basicauth + htpasswd -d -b -c /srv/app/.htpasswd $HTPASSWD_USER $HTPASSWD_PASSWORD + # Start uwsgi with basicauth + uwsgi --ini /srv/app/uwsgi.conf --pcre-jit $UWSGI_OPTS + else + echo "Missing HTPASSWD_USER or HTPASSWD_PASSWORD environment variables. Exiting..." + exit 1 + fi + else + # Start uwsgi + uwsgi $UWSGI_OPTS + fi +else + echo "[prerun] failed...not starting CKAN." +fi diff --git a/rootfs/setup/app/uwsgi.conf b/rootfs/setup/app/uwsgi.conf new file mode 100644 index 0000000..6321d6d --- /dev/null +++ b/rootfs/setup/app/uwsgi.conf @@ -0,0 +1,2 @@ +[uwsgi] +route = ^(?!/api).*$ basicauth:Restricted,/srv/app/.htpasswd diff --git a/rootfs/setup/nginx.conf b/rootfs/setup/nginx.conf deleted file mode 100644 index ca21636..0000000 --- a/rootfs/setup/nginx.conf +++ /dev/null @@ -1,41 +0,0 @@ -worker_processes 4; - -events { - worker_connections 1024; -} - -http { - include mime.types; - default_type application/octet-stream; - - sendfile on; - client_max_body_size 100m; - keepalive_timeout 65; - - server { - listen 5000; - server_name localhost; - - auth_basic "Restricted"; - auth_basic_user_file /srv/app/.htpasswd; - - location / { - auth_basic "Restricted"; - auth_basic_user_file /srv/app/.htpasswd; - proxy_pass http://127.0.0.1:4000/; - proxy_redirect off; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header Host $host; - } - - location /api/3/action/status_show { - auth_basic "off"; - proxy_pass http://127.0.0.1:4000/api/3/action/status_show; - proxy_redirect off; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header Host $host; - } - } -} diff --git a/rootfs/setup/start_ckan.sh b/rootfs/setup/start_ckan.sh deleted file mode 100755 index 4c76d16..0000000 --- a/rootfs/setup/start_ckan.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -python prerun.py -if [ $? -eq 0 ] -then - if [ "$PASSWORD_PROTECT" = true ] - then - if [ "$HTPASSWD_USER" ] || [ "$HTPASSWD_PASSWORD" ] - then - cp -a /srv/app/nginx.conf /etc/nginx/nginx.conf - htpasswd -b -c /srv/app/.htpasswd $HTPASSWD_USER $HTPASSWD_PASSWORD - nginx - gunicorn --log-file=- -k gevent -w 4 -b 127.0.0.1:4000 --paste production.ini - else - echo "Missing HTPASSWD_USER or HTPASSWD_PASSWORD environment variables. Exiting..." - exit 1 - fi - else - gunicorn --log-file=- -k gevent -w 4 -b 0.0.0.0:5000 --paste production.ini - fi -else - echo "[prerun] failed...not starting CKAN." -fi From 5f8d060fc329665e60208ae06821817beab98ac0 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 18 Oct 2017 21:21:29 +0200 Subject: [PATCH 020/213] Move to a multi-stage docker setup --- rootfs/Dockerfile | 122 ++++++++++++++++++++------------- rootfs/setup/app/start_ckan.sh | 2 +- 2 files changed, 74 insertions(+), 50 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index f9073e7..eb736d7 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -1,31 +1,23 @@ -FROM keitaro/base:0.2 +################## +### Build CKAN ### +################## +FROM alpine:3.6 as ckanbuild -MAINTAINER Keitaro Inc +# Set CKAN version to build +ENV GIT_URL=https://github.com/ckan/ckan.git +ENV GIT_BRANCH=ckan-2.7.2 -ENV APP_DIR=/srv/app +# Set src dirs ENV SRC_DIR=/srv/app/src ENV PIP_SRC=${SRC_DIR} -ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.6.2 -ENV CKAN_SITE_URL=http://localhost:5000 -ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars -WORKDIR ${APP_DIR} +WORKDIR ${SRC_DIR} -######################### -### Base docker layer ### -######################### -# Create a local user and group to run the app -RUN addgroup -g 92 -S ckan && \ - adduser -u 92 -h /srv/app -H -D -S -G ckan ckan -# Install necessary packages to run CKAN -RUN apk add --no-cache git \ - gettext \ - postgresql-client \ +# Packages to build CKAN requirements and plugins +RUN apk add --no-cache \ + git \ + curl \ python \ - apache2-utils && \ - # Packages to build CKAN requirements and plugins - apk add --no-cache --virtual .build-deps \ postgresql-dev \ linux-headers \ gcc \ @@ -33,38 +25,72 @@ RUN apk add --no-cache git \ g++ \ autoconf \ automake \ - libtool \ + libtool \ musl-dev \ pcre-dev \ - python-dev && \ + python-dev + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Fetch and build CKAN and requirements +RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan +RUN rm -rf /srv/app/src/ckan/.git +RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt +RUN pip wheel --wheel-dir=/wheels uwsgi gevent + +############ +### MAIN ### +############ +FROM alpine:3.6 + +MAINTAINER Keitaro Inc + +ENV APP_DIR=/srv/app +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} +ENV CKAN_SITE_URL=http://localhost:5000 +ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars + +WORKDIR ${APP_DIR} + +# Install necessary packages to run CKAN +RUN apk add --no-cache git \ + gettext \ + curl \ + postgresql-client \ + python \ + apache2-utils && \ # Create SRC_DIR - mkdir -p ${SRC_DIR} && \ - # Install pip and uwsgi - curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ - python ${SRC_DIR}/get-pip.py && \ - pip install --no-cache-dir uwsgi gevent && \ - rm -rf ${SRC_DIR}/get-pip.py + mkdir -p ${SRC_DIR} -############################ -### libgeos docker layer ### -############################ -# Build and install libgeos to support geospatial -RUN git clone -b 3.6.0 --depth=1 --single-branch https://git.osgeo.org/gogs/geos/geos.git ${SRC_DIR}/geos && \ - cd ${SRC_DIR}/geos && \ - ./autogen.sh && \ - ./configure --prefix /usr && \ - make -j2 && \ - make install && \ - rm -rf ${SRC_DIR}/geos +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Get artifacts from build stages +COPY --from=ckanbuild /wheels /srv/app/wheels +COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan + +# Copy necessary scripts +COPY setup/app ${APP_DIR} + +# Additional install steps for build stages artifacts +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent + +# Create a local user and group to run the app +RUN addgroup -g 92 -S ckan && \ + adduser -u 92 -h /srv/app -H -D -S -G ckan ckan -######################### -### CKAN docker layer ### -######################### # Install CKAN -RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan && \ +RUN pip install -e /srv/app/src/ckan && \ cd ${SRC_DIR}/ckan && \ cp who.ini ${APP_DIR} && \ - pip install -r requirements.txt && \ + pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ # Install CKAN envvars to support loading config from environment variables pip install -e git+https://github.com/okfn/ckanext-envvars.git@0.0.1#egg=ckanext-envvars && \ # Create and update CKAN config @@ -72,10 +98,8 @@ RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Change ownership to app user chown -R ckan:ckan /srv/app && \ - # Clear the .git directory - rm -rf /srv/app/src/ckan/.git - -COPY setup/app ${APP_DIR} + # Remove wheels + rm -rf /srv/app/wheels EXPOSE 5000 diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh index d9fb405..5849813 100755 --- a/rootfs/setup/app/start_ckan.sh +++ b/rootfs/setup/app/start_ckan.sh @@ -3,7 +3,7 @@ python prerun.py # Set the common uwsgi options -UWSGI_OPTS="--socket /tmp/uwsgi.sock --thunder-lock --uid 92 --gid 92 --http :5000 --master --single-interpreter --enable-threads --paste config:/srv/app/production.ini --gevent 2000 -p 4 -L" +UWSGI_OPTS="--plugins http,python,gevent --socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L" # Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully if [ $? -eq 0 ] From 42d17a659d47304eea63038fa57d18c8bf881cf1 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Tue, 22 May 2018 14:53:09 +0200 Subject: [PATCH 021/213] Docker for ckan 2.8.0 --- rootfs/Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index eb736d7..431daf2 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -1,11 +1,11 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.6 as ckanbuild +FROM alpine:3.7 as ckanbuild # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.7.2 +ENV GIT_BRANCH=ckan-2.8.0 # Set src dirs ENV SRC_DIR=/srv/app/src @@ -46,7 +46,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi gevent ############ ### MAIN ### ############ -FROM alpine:3.6 +FROM alpine:3.7 MAINTAINER Keitaro Inc @@ -64,6 +64,7 @@ RUN apk add --no-cache git \ curl \ postgresql-client \ python \ + libmagic \ apache2-utils && \ # Create SRC_DIR mkdir -p ${SRC_DIR} From 1d4d2759a1c28a233926fb1ee66f27fe86f549b1 Mon Sep 17 00:00:00 2001 From: Pance Ribarski Date: Fri, 29 Jun 2018 18:53:51 +0200 Subject: [PATCH 022/213] added extra_scripts.sh before ckan starts --- rootfs/setup/app/extra_scripts.sh | 4 ++++ rootfs/setup/app/start_ckan.sh | 1 + 2 files changed, 5 insertions(+) create mode 100755 rootfs/setup/app/extra_scripts.sh diff --git a/rootfs/setup/app/extra_scripts.sh b/rootfs/setup/app/extra_scripts.sh new file mode 100755 index 0000000..80a70ef --- /dev/null +++ b/rootfs/setup/app/extra_scripts.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# this is called before uwsgi is executed +# uset his to add extra scripts before ckan is started diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh index 5849813..ca5799e 100755 --- a/rootfs/setup/app/start_ckan.sh +++ b/rootfs/setup/app/start_ckan.sh @@ -8,6 +8,7 @@ UWSGI_OPTS="--plugins http,python,gevent --socket /tmp/uwsgi.sock --uid 92 --gid # Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully if [ $? -eq 0 ] then + extra_scripts.sh if [ "$PASSWORD_PROTECT" = true ] then if [ "$HTPASSWD_USER" ] || [ "$HTPASSWD_PASSWORD" ] From 59e403a760b284f7a95008aa621a24228b2502c9 Mon Sep 17 00:00:00 2001 From: Atanas Kostovski Date: Mon, 30 Jul 2018 14:20:13 +0200 Subject: [PATCH 023/213] Update to ckan 2.8.1 and alpine 3.8 --- rootfs/Dockerfile | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 431daf2..532d3a5 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -1,11 +1,11 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.7 as ckanbuild +FROM alpine:3.8 as ckanbuild # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.0 +ENV GIT_BRANCH=ckan-2.8.1 # Set src dirs ENV SRC_DIR=/srv/app/src @@ -46,7 +46,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi gevent ############ ### MAIN ### ############ -FROM alpine:3.7 +FROM alpine:3.8 MAINTAINER Keitaro Inc @@ -98,9 +98,10 @@ RUN pip install -e /srv/app/src/ckan && \ paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Change ownership to app user - chown -R ckan:ckan /srv/app && \ - # Remove wheels - rm -rf /srv/app/wheels + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/wheels EXPOSE 5000 From 3f43bca34908c27c97f5e362d64936a5e29de806 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 2 Aug 2018 10:34:55 +0200 Subject: [PATCH 024/213] Install bash in base image as it's required by the start_ckan.sh script --- rootfs/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 532d3a5..abd68da 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -60,11 +60,12 @@ WORKDIR ${APP_DIR} # Install necessary packages to run CKAN RUN apk add --no-cache git \ + bash \ gettext \ curl \ postgresql-client \ python \ - libmagic \ + libmagic \ apache2-utils && \ # Create SRC_DIR mkdir -p ${SRC_DIR} From c0e229197efc4207cda126998fbf1f22ff564543 Mon Sep 17 00:00:00 2001 From: amercader Date: Tue, 7 Aug 2018 11:58:04 +0200 Subject: [PATCH 025/213] Provide a better way to run extra scripts from children images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current way requires an `extra_scripts.sh` file to be present in on children images. With this approach a new folder `/docker-entrypoint.d` is created when an image extends this one. The children image can then copy any shell or python file that they want executed at the start of container in that directory. For instance consider the following setup: ``` cloud-mysite ├── .env ├── docker-entrypoint.d │   └── 00_setup_validation.sh │   └── 01_check_users.py └── Dockerfile ``` The `Dockerfile` will look like: ``` FROM keitaro/ckan:2.8.1 [...] COPY docker-entrypoint.d/* /docker-entrypoint.d/ ``` The files will be executed in alphabetical order. --- rootfs/Dockerfile | 3 +++ rootfs/setup/app/start_ckan.sh | 14 +++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 532d3a5..805203b 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -103,6 +103,9 @@ RUN pip install -e /srv/app/src/ckan && \ # Remove wheels RUN rm -rf /srv/app/wheels +# Create entrypoint directory for children image scripts +ONBUILD RUN mkdir /docker-entrypoint.d + EXPOSE 5000 HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh index ca5799e..ef8f1d4 100755 --- a/rootfs/setup/app/start_ckan.sh +++ b/rootfs/setup/app/start_ckan.sh @@ -2,13 +2,25 @@ # Run the prerun script to init CKAN and create the default admin user python prerun.py +# Run any startup scripts provided by images extending this one +if [[ -d "/docker-entrypoint.d" ]] +then + for f in /docker-entrypoint.d/*; do + case "$f" in + *.sh) echo "$0: Running init file $f"; . "$f" ;; + *.py) echo "$0: Running init file $f"; python "$f"; echo ;; + *) echo "$0: Ignoring $f (not an sh or py file)" ;; + esac + echo + done +fi + # Set the common uwsgi options UWSGI_OPTS="--plugins http,python,gevent --socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L" # Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully if [ $? -eq 0 ] then - extra_scripts.sh if [ "$PASSWORD_PROTECT" = true ] then if [ "$HTPASSWD_USER" ] || [ "$HTPASSWD_PASSWORD" ] From d643d6eb59c3198ecf1b5e86cd6879244fbcae11 Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 8 Aug 2018 15:09:45 +0200 Subject: [PATCH 026/213] Fix sysadmin creation on 2.8 Whitespace changes meant that the check to see if the sysadmin exists no longer works on CKAN 2.8. --- rootfs/setup/app/prerun.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rootfs/setup/app/prerun.py b/rootfs/setup/app/prerun.py index 21dc9b1..ab0c51e 100644 --- a/rootfs/setup/app/prerun.py +++ b/rootfs/setup/app/prerun.py @@ -3,6 +3,7 @@ import sys import subprocess import psycopg2 import urllib2 +import re ckan_ini = os.environ.get('CKAN_INI', '/srv/app/production.ini') @@ -85,7 +86,7 @@ def create_sysadmin(): command = ['paster', '--plugin=ckan', 'user', name, '-c', ckan_ini] out = subprocess.check_output(command) - if 'User: \nNone\n' not in out: + if 'User:None' not in re.sub(r'\s', '', out): print '[prerun] Sysadmin user exists, skipping creation' return From 2e24eeae19ea1f3bc60d26aee44cbc606c9a7d4e Mon Sep 17 00:00:00 2001 From: Pance Ribarski Date: Fri, 10 Aug 2018 11:10:50 +0200 Subject: [PATCH 027/213] fix calling extra-scripts.sh --- rootfs/setup/app/start_ckan.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh index ca5799e..e9cae46 100755 --- a/rootfs/setup/app/start_ckan.sh +++ b/rootfs/setup/app/start_ckan.sh @@ -8,7 +8,7 @@ UWSGI_OPTS="--plugins http,python,gevent --socket /tmp/uwsgi.sock --uid 92 --gid # Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully if [ $? -eq 0 ] then - extra_scripts.sh + /bin/sh extra_scripts.sh if [ "$PASSWORD_PROTECT" = true ] then if [ "$HTPASSWD_USER" ] || [ "$HTPASSWD_PASSWORD" ] From 6bc2cae8d2afe587b1b294ac2cfd4359c86f5d75 Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 13 Aug 2018 15:23:47 +0200 Subject: [PATCH 028/213] Remove uWSGI --plugins options As we are now installing uwsgi via pip these plugins are shipped as part of the code and there's no need to load the plugin, otherwise you get these alarming (but harmless) messages: open("./http_plugin.so"): No such file or directory [core/utils.c line 3721] !!! UNABLE to load uWSGI plugin: Error loading shared library ./http_plugin.so: No such file or directory !!! open("./python_plugin.so"): No such file or directory [core/utils.c line 3721] !!! UNABLE to load uWSGI plugin: Error loading shared library ./python_plugin.so: No such file or directory !!! open("./gevent_plugin.so"): No such file or directory [core/utils.c line 3721] !!! UNABLE to load uWSGI plugin: Error loading shared library ./gevent_plugin.so: No such file or directory !!! --- rootfs/setup/app/start_ckan.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh index ca5799e..8f103c2 100755 --- a/rootfs/setup/app/start_ckan.sh +++ b/rootfs/setup/app/start_ckan.sh @@ -3,7 +3,7 @@ python prerun.py # Set the common uwsgi options -UWSGI_OPTS="--plugins http,python,gevent --socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L" +UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L" # Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully if [ $? -eq 0 ] From 754f201942f49e8bd518cab0ac2e9ad02dbef33c Mon Sep 17 00:00:00 2001 From: Dragan Bocevski Date: Fri, 4 Jan 2019 13:46:03 +0100 Subject: [PATCH 029/213] ckan v2.8.2 --- rootfs/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 67f2721..3fbe5a0 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -5,7 +5,7 @@ FROM alpine:3.8 as ckanbuild # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.1 +ENV GIT_BRANCH=ckan-2.8.2 # Set src dirs ENV SRC_DIR=/srv/app/src From e47474de97574c2d57851ac0388bb582df79ecfd Mon Sep 17 00:00:00 2001 From: Dragan Bocevski Date: Thu, 10 Jan 2019 23:25:02 +0100 Subject: [PATCH 030/213] Update SOLR check in prerun --- rootfs/setup/app/prerun.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/rootfs/setup/app/prerun.py b/rootfs/setup/app/prerun.py index ab0c51e..2bd30d4 100644 --- a/rootfs/setup/app/prerun.py +++ b/rootfs/setup/app/prerun.py @@ -5,6 +5,7 @@ import psycopg2 import urllib2 import re +import time ckan_ini = os.environ.get('CKAN_INI', '/srv/app/production.ini') @@ -12,6 +13,8 @@ RETRY = 5 def check_db_connection(retry=None): + print '[prerun] Start check_db_connection...' + if retry is None: retry = RETRY elif retry == 0: @@ -33,6 +36,8 @@ def check_db_connection(retry=None): def check_solr_connection(retry=None): + print '[prerun] Start check_solr_connection...' + if retry is None: retry = RETRY elif retry == 0: @@ -51,16 +56,22 @@ def check_solr_connection(retry=None): time.sleep(10) check_solr_connection(retry = retry - 1) else: - eval(connection.read()) - + import re + conn_info = connection.read() + conn_info = re.sub(r'"zkConnected":true', '"zkConnected":True', conn_info) + eval(conn_info) def init_db(): - db_command = ['paster', '--plugin=ckan', 'db', - 'init', '-c', ckan_ini] - print '[prerun] Initializing or upgrading db - start' + print '[prerun] Start init_db...' + + db_command = ['paster', '--plugin=ckan', 'db', 'init', '-c', ckan_ini] + + print '[prerun] Initializing or upgrading db - start using paster db init' try: + # run init scripts subprocess.check_output(db_command, stderr=subprocess.STDOUT) + print '[prerun] Initializing or upgrading db - end' except subprocess.CalledProcessError, e: if 'OperationalError' in e.output: @@ -72,10 +83,12 @@ def init_db(): else: print e.output raise e - + print '[prerun] Initializing or upgrading db - finish' def create_sysadmin(): + print '[prerun] Start create_sysadmin...' + name = os.environ.get('CKAN_SYSADMIN_NAME') password = os.environ.get('CKAN_SYSADMIN_PASSWORD') email = os.environ.get('CKAN_SYSADMIN_EMAIL') @@ -108,7 +121,6 @@ def create_sysadmin(): subprocess.call(command) print '[prerun] Made user {0} a sysadmin'.format(name) - if __name__ == '__main__': maintenance = os.environ.get('MAINTENANCE_MODE', '').lower() == 'true' @@ -120,3 +132,4 @@ if __name__ == '__main__': check_solr_connection() init_db() create_sysadmin() + #time.sleep(60000) # don't end the prerun script to allow container dock and debug From b84427ec8326c02d04201e5ba622cef0f4e34321 Mon Sep 17 00:00:00 2001 From: Dragan Bocevski Date: Thu, 10 Jan 2019 23:25:29 +0100 Subject: [PATCH 031/213] Add pcre to main stage and change order of default CKAN plugins --- rootfs/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 3fbe5a0..05fd3e6 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -28,6 +28,7 @@ RUN apk add --no-cache \ libtool \ musl-dev \ pcre-dev \ + pcre \ python-dev # Create the src directory @@ -54,7 +55,7 @@ ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 -ENV CKAN__PLUGINS image_view text_view recline_view datastore datapusher envvars +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher WORKDIR ${APP_DIR} @@ -66,6 +67,7 @@ RUN apk add --no-cache git \ postgresql-client \ python \ libmagic \ + pcre \ apache2-utils && \ # Create SRC_DIR mkdir -p ${SRC_DIR} From 0c3ea651a3fcf355090179593d42694c6b80ee86 Mon Sep 17 00:00:00 2001 From: DraganBocevski <36728855+DraganBocevski@users.noreply.github.com> Date: Fri, 8 Feb 2019 18:30:53 +0100 Subject: [PATCH 032/213] added filestore plugin Needed for upload functionality --- rootfs/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 05fd3e6..52788a7 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -55,7 +55,7 @@ ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 -ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher filestore WORKDIR ${APP_DIR} From 613ce58dfa2e110ed103ea6b8855717ab2c7e7c2 Mon Sep 17 00:00:00 2001 From: Dragan Bocevski Date: Sat, 9 Feb 2019 14:48:56 +0100 Subject: [PATCH 033/213] filestore is core plugin, no need to include it as external plugin --- rootfs/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 52788a7..05fd3e6 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -55,7 +55,7 @@ ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 -ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher filestore +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher WORKDIR ${APP_DIR} From 1d5700b84221e647c168c79a1850e20ea013ae69 Mon Sep 17 00:00:00 2001 From: Konstantin Sivakov Date: Mon, 18 Feb 2019 22:51:35 +0100 Subject: [PATCH 034/213] Datastore creating at prerun with set permissions --- rootfs/setup/app/prerun.py | 55 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/rootfs/setup/app/prerun.py b/rootfs/setup/app/prerun.py index 2bd30d4..2b4eb62 100644 --- a/rootfs/setup/app/prerun.py +++ b/rootfs/setup/app/prerun.py @@ -66,12 +66,12 @@ def init_db(): print '[prerun] Start init_db...' db_command = ['paster', '--plugin=ckan', 'db', 'init', '-c', ckan_ini] - + print '[prerun] Initializing or upgrading db - start using paster db init' try: # run init scripts subprocess.check_output(db_command, stderr=subprocess.STDOUT) - + print '[prerun] Initializing or upgrading db - end' except subprocess.CalledProcessError, e: if 'OperationalError' in e.output: @@ -85,6 +85,55 @@ def init_db(): raise e print '[prerun] Initializing or upgrading db - finish' + +def init_datastore(): + + conn_str = os.environ.get('CKAN_DATASTORE_WRITE_URL') + if not conn_str: + print '[prerun] Skipping datastore initialization' + return + + datastore_perms_command = ['paster', '--plugin=ckan', 'datastore', + 'set-permissions', '-c', ckan_ini] + + connection = psycopg2.connect(conn_str) + cursor = connection.cursor() + + print '[prerun] Initializing datastore db - start' + try: + datastore_perms = subprocess.Popen( + datastore_perms_command, + stdout=subprocess.PIPE) + + perms_sql = datastore_perms.stdout.read() + # Remove internal pg command as psycopg2 does not like it + perms_sql = re.sub('\\\\connect \"(.*)\"', '', perms_sql) + cursor.execute(perms_sql) + for notice in connection.notices: + print notice + + connection.commit() + + print '[prerun] Initializing datastore db - end' + print datastore_perms.stdout.read() + except psycopg2.Error as e: + print '[prerun] Could not initialize datastore' + print str(e) + + except subprocess.CalledProcessError, e: + if 'OperationalError' in e.output: + print e.output + print '[prerun] Database not ready, waiting a bit before exit...' + time.sleep(5) + sys.exit(1) + else: + print e.output + raise e + finally: + cursor.close() + connection.close() + + def create_sysadmin(): print '[prerun] Start create_sysadmin...' @@ -131,5 +180,7 @@ if __name__ == '__main__': check_db_connection() check_solr_connection() init_db() + if os.environ.get('CKAN_DATASTORE_WRITE_URL'): + init_datastore() create_sysadmin() #time.sleep(60000) # don't end the prerun script to allow container dock and debug From 79c28db2dc608cc01fbb21c2f1349e1a719e27c6 Mon Sep 17 00:00:00 2001 From: Konstantin Sivakov Date: Tue, 19 Feb 2019 11:02:17 +0100 Subject: [PATCH 035/213] Access denied bug fix --- rootfs/Dockerfile | 2 +- rootfs/setup/app/start_ckan.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 05fd3e6..ee341e5 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -107,7 +107,7 @@ RUN pip install -e /srv/app/src/ckan && \ RUN rm -rf /srv/app/wheels # Create entrypoint directory for children image scripts -ONBUILD RUN mkdir /docker-entrypoint.d +ONBUILD RUN mkdir docker-entrypoint.d EXPOSE 5000 diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh index d0dc6e7..e2e69a8 100755 --- a/rootfs/setup/app/start_ckan.sh +++ b/rootfs/setup/app/start_ckan.sh @@ -3,7 +3,7 @@ python prerun.py # Run any startup scripts provided by images extending this one -if [[ -d "/docker-entrypoint.d" ]] +if [[ -d "docker-entrypoint.d" ]] then for f in /docker-entrypoint.d/*; do case "$f" in From d76095408bb2639c058781716d68b0533034b65f Mon Sep 17 00:00:00 2001 From: Zoran Pandovski Date: Fri, 8 Mar 2019 14:27:11 +0100 Subject: [PATCH 036/213] Create Readme.md --- Readme.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Readme.md diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..1e58126 --- /dev/null +++ b/Readme.md @@ -0,0 +1,58 @@ +# Docker ckan image ![Docker Pulls](https://img.shields.io/docker/pulls/keitaro/ckan.svg) + +## Overview + +This repository contains base docker image used to build CKAN instances. It's based on [Alpine Linux](https://alpinelinux.org/) and includes only required extensions to start CKAN instance. + +## Build + +To create new image run: + +```sh +docker build --tag ckan-2.8.2 . +``` +The –-tag ckan-2.8.2 flag sets the image name to ckan-2.8.2 and the dot ( “.” ) at the end tells docker build to look into the current directory for Dockerfile and related contents. + +## List + +Check if the image shows up in the list of images: +```sh + docker images +``` + +## Run + +To start and test newly created image run: +```sh + docker run ckan-2.8.2 +``` +Check if CKAN was succesfuly started on http://localhost:5000. The ckan site url is configured in ENV CKAN_SITE_URL. + + +## Upload to DockerHub + +>*It's recommended to upload built images to DockerHub* + +To upload the image to DockerHub run: + +```sh +docker push [options] /ckan: +``` + +## Upgrade +To upgrade the Docker file to use new CKAN version, in the Dockerfile you should change: + +>ENV GIT_BRANCH={ckan_release} + +Check [CKAN repository](https://github.com/ckan/ckan/releases) for the latest releases. +If there are new libraries used by the new version requirements, those needs to be included too. + +## Extensions + +Default extensions used in the Dockerfile are kept in: + +>ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher + +## Add new scripts + +You can add scripts to CKAN custom images and copy them to the *docker-entrypoint.d* directory. Any *.sh or *.py file in that directory will be executed after the main initialization script (prerun.py) is executed. From f80c60f8359d3d053622e356086fb148b761d62c Mon Sep 17 00:00:00 2001 From: Shahar Evron Date: Thu, 3 Oct 2019 12:00:34 +0300 Subject: [PATCH 037/213] Fix check for `docker-entrypoint.d` The entrypoint script checks for the existence of `docker-entrypoint.d` in the CWD, but then run the scripts in `/docker-entrypoint.d`. This causes the scripts to either not run at all or fail if an inheriting image changes the `WORKDIR`. This fix makes sure the directory is always expected in the filesystem root. --- rootfs/setup/app/start_ckan.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh index e2e69a8..d0dc6e7 100755 --- a/rootfs/setup/app/start_ckan.sh +++ b/rootfs/setup/app/start_ckan.sh @@ -3,7 +3,7 @@ python prerun.py # Run any startup scripts provided by images extending this one -if [[ -d "docker-entrypoint.d" ]] +if [[ -d "/docker-entrypoint.d" ]] then for f in /docker-entrypoint.d/*; do case "$f" in From 1bf976578be95fbe8cae9d1b5783c5d771e5e6d8 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Mon, 17 Feb 2020 15:30:52 +0100 Subject: [PATCH 038/213] Upgrade to python3 to support up and coming CKAN 2.9 version --- rootfs/Dockerfile | 40 +++++++++++----- rootfs/setup/app/prerun.py | 93 ++++++++++++++++++-------------------- 2 files changed, 73 insertions(+), 60 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index ee341e5..f9d492c 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -1,11 +1,11 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.8 as ckanbuild +FROM alpine:3.11 as ckanbuild # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.2 +ENV GIT_BRANCH=master # Set src dirs ENV SRC_DIR=/srv/app/src @@ -17,7 +17,7 @@ WORKDIR ${SRC_DIR} RUN apk add --no-cache \ git \ curl \ - python \ + python3 \ postgresql-dev \ linux-headers \ gcc \ @@ -29,7 +29,12 @@ RUN apk add --no-cache \ musl-dev \ pcre-dev \ pcre \ - python-dev + python3-dev \ + libxml2-dev \ + libxslt-dev + +# Link python to python3 +RUN ln -s /usr/bin/python3 /usr/bin/python # Create the src directory RUN mkdir -p ${SRC_DIR} @@ -47,7 +52,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi gevent ############ ### MAIN ### ############ -FROM alpine:3.8 +FROM alpine:3.11 MAINTAINER Keitaro Inc @@ -65,12 +70,17 @@ RUN apk add --no-cache git \ gettext \ curl \ postgresql-client \ - python \ + python3 \ libmagic \ pcre \ + libxslt \ + libxml2 \ + tzdata \ apache2-utils && \ # Create SRC_DIR - mkdir -p ${SRC_DIR} + mkdir -p ${SRC_DIR} && \ + # Link python to python3 + ln -s /usr/bin/python3 /usr/bin/python # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ @@ -80,9 +90,6 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ COPY --from=ckanbuild /wheels /srv/app/wheels COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan -# Copy necessary scripts -COPY setup/app ${APP_DIR} - # Additional install steps for build stages artifacts RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent @@ -98,14 +105,23 @@ RUN pip install -e /srv/app/src/ckan && \ # Install CKAN envvars to support loading config from environment variables pip install -e git+https://github.com/okfn/ckanext-envvars.git@0.0.1#egg=ckanext-envvars && \ # Create and update CKAN config - paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ - paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + # ckan generate config requires cookiecutter which is in the dev-requirements, temp workaround + pip install cookiecutter==1.6.0 && \ + # Set timezone + echo "Europe/Stockholm" > /etc/timezone && \ + ckan generate config ${APP_DIR}/production.ini && \ + # Not working atm since ckan config tool tries to load config before executing config-tool, workaround + #ckan -c ${APP_DIR}/production.ini config-tool "ckan.plugins = ${CKAN__PLUGINS}" && \ + sed -i "/ckan.plugins = stats/c ckan.plugins = ${CKAN__PLUGINS}" ${APP_DIR}/production.ini && \ # Change ownership to app user chown -R ckan:ckan /srv/app # Remove wheels RUN rm -rf /srv/app/wheels +# Copy necessary scripts +COPY setup/app ${APP_DIR} + # Create entrypoint directory for children image scripts ONBUILD RUN mkdir docker-entrypoint.d diff --git a/rootfs/setup/app/prerun.py b/rootfs/setup/app/prerun.py index 2b4eb62..5f55f70 100644 --- a/rootfs/setup/app/prerun.py +++ b/rootfs/setup/app/prerun.py @@ -2,7 +2,7 @@ import os import sys import subprocess import psycopg2 -import urllib2 +import urllib.request, urllib.error, urllib.parse import re import time @@ -13,12 +13,12 @@ RETRY = 5 def check_db_connection(retry=None): - print '[prerun] Start check_db_connection...' + print('[prerun] Start check_db_connection...') if retry is None: retry = RETRY elif retry == 0: - print '[prerun] Giving up after 5 tries...' + print('[prerun] Giving up after 5 tries...') sys.exit(1) conn_str = os.environ.get('CKAN_SQLALCHEMY_URL', '') @@ -26,8 +26,8 @@ def check_db_connection(retry=None): connection = psycopg2.connect(conn_str) except psycopg2.Error as e: - print str(e) - print '[prerun] Unable to connect to the database...try again in a while.' + print((str(e))) + print('[prerun] Unable to connect to the database...try again in a while.') import time time.sleep(10) check_db_connection(retry = retry - 1) @@ -36,70 +36,69 @@ def check_db_connection(retry=None): def check_solr_connection(retry=None): - print '[prerun] Start check_solr_connection...' + print('[prerun] Start check_solr_connection...') if retry is None: retry = RETRY elif retry == 0: - print '[prerun] Giving up after 5 tries...' + print('[prerun] Giving up after 5 tries...') sys.exit(1) url = os.environ.get('CKAN_SOLR_URL', '') search_url = '{url}/select/?q=*&wt=json'.format(url=url) try: - connection = urllib2.urlopen(search_url) - except urllib2.URLError as e: - print str(e) - print '[prerun] Unable to connect to solr...try again in a while.' + connection = urllib.request.urlopen(search_url) + except urllib.error.URLError as e: + print((str(e))) + print('[prerun] Unable to connect to solr...try again in a while.') import time time.sleep(10) check_solr_connection(retry = retry - 1) else: import re conn_info = connection.read() - conn_info = re.sub(r'"zkConnected":true', '"zkConnected":True', conn_info) eval(conn_info) def init_db(): - print '[prerun] Start init_db...' + print('[prerun] Start init_db...') - db_command = ['paster', '--plugin=ckan', 'db', 'init', '-c', ckan_ini] + db_command = ['ckan', '-c', ckan_ini, 'db', 'init'] - print '[prerun] Initializing or upgrading db - start using paster db init' + print('[prerun] Initializing or upgrading db - start using ckan db init') try: # run init scripts subprocess.check_output(db_command, stderr=subprocess.STDOUT) - print '[prerun] Initializing or upgrading db - end' - except subprocess.CalledProcessError, e: + print('[prerun] Initializing or upgrading db - end') + except subprocess.CalledProcessError as e: if 'OperationalError' in e.output: - print e.output - print '[prerun] Database not ready, waiting a bit before exit...' + print((e.output)) + print('[prerun] Database not ready, waiting a bit before exit...') import time time.sleep(5) sys.exit(1) else: - print e.output + print((e.output)) raise e - print '[prerun] Initializing or upgrading db - finish' + print('[prerun] Initializing or upgrading db - finish') def init_datastore(): conn_str = os.environ.get('CKAN_DATASTORE_WRITE_URL') if not conn_str: - print '[prerun] Skipping datastore initialization' + print('[prerun] Skipping datastore initialization') return - datastore_perms_command = ['paster', '--plugin=ckan', 'datastore', - 'set-permissions', '-c', ckan_ini] + datastore_perms_command = ['ckan', '-c', ckan_ini, 'datastore', + 'set-permissions'] connection = psycopg2.connect(conn_str) cursor = connection.cursor() - print '[prerun] Initializing datastore db - start' + print('[prerun] Initializing datastore db - start') try: datastore_perms = subprocess.Popen( datastore_perms_command, @@ -107,27 +106,27 @@ def init_datastore(): perms_sql = datastore_perms.stdout.read() # Remove internal pg command as psycopg2 does not like it - perms_sql = re.sub('\\\\connect \"(.*)\"', '', perms_sql) + perms_sql = re.sub('\\\\connect \"(.*)\"', '', perms_sql.decode('utf-8')) cursor.execute(perms_sql) for notice in connection.notices: - print notice + print(notice) connection.commit() - print '[prerun] Initializing datastore db - end' - print datastore_perms.stdout.read() + print('[prerun] Initializing datastore db - end') + print((datastore_perms.stdout.read())) except psycopg2.Error as e: - print '[prerun] Could not initialize datastore' - print str(e) + print('[prerun] Could not initialize datastore') + print((str(e))) - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: if 'OperationalError' in e.output: - print e.output - print '[prerun] Database not ready, waiting a bit before exit...' + print((e.output)) + print('[prerun] Database not ready, waiting a bit before exit...') time.sleep(5) sys.exit(1) else: - print e.output + print((e.output)) raise e finally: cursor.close() @@ -136,7 +135,7 @@ def init_datastore(): def create_sysadmin(): - print '[prerun] Start create_sysadmin...' + print('[prerun] Start create_sysadmin...') name = os.environ.get('CKAN_SYSADMIN_NAME') password = os.environ.get('CKAN_SYSADMIN_PASSWORD') @@ -145,37 +144,35 @@ def create_sysadmin(): if name and password and email: # Check if user exists - command = ['paster', '--plugin=ckan', 'user', name, '-c', ckan_ini] + command = ['ckan', '-c', ckan_ini, 'user', 'show', name] out = subprocess.check_output(command) - if 'User:None' not in re.sub(r'\s', '', out): - print '[prerun] Sysadmin user exists, skipping creation' + if 'User:None' not in re.sub(r'\s', '', out.decode('utf-8')): + print('[prerun] Sysadmin user exists, skipping creation') return # Create user - command = ['paster', '--plugin=ckan', 'user', 'add', + command = ['ckan', '-c', ckan_ini, 'user', 'add', name, 'password=' + password, - 'email=' + email, - '-c', ckan_ini] + 'email=' + email] subprocess.call(command) - print '[prerun] Created user {0}'.format(name) + print(('[prerun] Created user {0}'.format(name))) # Make it sysadmin - command = ['paster', '--plugin=ckan', 'sysadmin', 'add', - name, - '-c', ckan_ini] + command = ['ckan', '-c', ckan_ini, 'sysadmin', 'add', + name] subprocess.call(command) - print '[prerun] Made user {0} a sysadmin'.format(name) + print(('[prerun] Made user {0} a sysadmin'.format(name))) if __name__ == '__main__': maintenance = os.environ.get('MAINTENANCE_MODE', '').lower() == 'true' if maintenance: - print '[prerun] Maintenance mode, skipping setup...' + print('[prerun] Maintenance mode, skipping setup...') else: check_db_connection() check_solr_connection() From 51d6dde825ab34509bea5685a1c8fd5ff02410d8 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 3 Jun 2020 09:53:14 +0200 Subject: [PATCH 039/213] Modular CKAN build and default extensions --- rootfs/Dockerfile | 77 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index f9d492c..e8d32b2 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -1,11 +1,11 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.11 as ckanbuild +FROM alpine:3.12 as ckanbuild # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=master +ENV GIT_BRANCH=ckan-2.8.4 # Set src dirs ENV SRC_DIR=/srv/app/src @@ -17,7 +17,7 @@ WORKDIR ${SRC_DIR} RUN apk add --no-cache \ git \ curl \ - python3 \ + python2 \ postgresql-dev \ linux-headers \ gcc \ @@ -29,13 +29,11 @@ RUN apk add --no-cache \ musl-dev \ pcre-dev \ pcre \ - python3-dev \ + python2-dev \ + libffi-dev \ libxml2-dev \ libxslt-dev -# Link python to python3 -RUN ln -s /usr/bin/python3 /usr/bin/python - # Create the src directory RUN mkdir -p ${SRC_DIR} @@ -49,10 +47,47 @@ RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt RUN pip wheel --wheel-dir=/wheels uwsgi gevent + +########################### +### Default-Extensions #### +########################### +FROM alpine:3.12 as extbuild + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +# List of default extensions +ENV DEFAULT_EXTENSIONS envvars s3filestore + +# Locations and tags, please use specific tags or revisions +ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars +ENV ENVVARS_GIT_BRANCH=0.0.1 +ENV S3FILESTORE_GIT_URL=https://github.com/okfn/ckanext-s3filestore +ENV S3FILESTORE_GIT_BRANCH=33d4b60 + +RUN apk add --no-cache \ + git \ + curl \ + python2 \ + python2-dev + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Fetch and build the default CKAN extensions +RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars +RUN pip wheel --wheel-dir=/wheels git+${S3FILESTORE_GIT_URL}@${S3FILESTORE_GIT_BRANCH}#egg=ckanext-s3filestore + + ############ ### MAIN ### ############ -FROM alpine:3.11 +FROM alpine:3.12 MAINTAINER Keitaro Inc @@ -60,7 +95,7 @@ ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 -ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher +ENV CKAN__PLUGINS envvars s3filestore image_view text_view recline_view datastore datapusher WORKDIR ${APP_DIR} @@ -70,7 +105,7 @@ RUN apk add --no-cache git \ gettext \ curl \ postgresql-client \ - python3 \ + python2 \ libmagic \ pcre \ libxslt \ @@ -78,9 +113,7 @@ RUN apk add --no-cache git \ tzdata \ apache2-utils && \ # Create SRC_DIR - mkdir -p ${SRC_DIR} && \ - # Link python to python3 - ln -s /usr/bin/python3 /usr/bin/python + mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ @@ -88,6 +121,7 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels +COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan # Additional install steps for build stages artifacts @@ -102,22 +136,19 @@ RUN pip install -e /srv/app/src/ckan && \ cd ${SRC_DIR}/ckan && \ cp who.ini ${APP_DIR} && \ pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ - # Install CKAN envvars to support loading config from environment variables - pip install -e git+https://github.com/okfn/ckanext-envvars.git@0.0.1#egg=ckanext-envvars && \ + # Install default CKAN extensions + pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars ckanext-s3filestore && \ # Create and update CKAN config - # ckan generate config requires cookiecutter which is in the dev-requirements, temp workaround - pip install cookiecutter==1.6.0 && \ # Set timezone - echo "Europe/Stockholm" > /etc/timezone && \ - ckan generate config ${APP_DIR}/production.ini && \ - # Not working atm since ckan config tool tries to load config before executing config-tool, workaround - #ckan -c ${APP_DIR}/production.ini config-tool "ckan.plugins = ${CKAN__PLUGINS}" && \ - sed -i "/ckan.plugins = stats/c ckan.plugins = ${CKAN__PLUGINS}" ${APP_DIR}/production.ini && \ + echo "UTC" > /etc/timezone && \ + # Generate CKAN config + paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ + paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Change ownership to app user chown -R ckan:ckan /srv/app # Remove wheels -RUN rm -rf /srv/app/wheels +RUN rm -rf /srv/app/wheels /srv/app/ext_wheels # Copy necessary scripts COPY setup/app ${APP_DIR} From ee215d0db5696e543d2c89eb3f6faf1f9723d19f Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Mon, 8 Jun 2020 11:45:16 +0200 Subject: [PATCH 040/213] First run docker entrypoint scripts before running prerun --- rootfs/setup/app/start_ckan.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh index d0dc6e7..8f71c90 100755 --- a/rootfs/setup/app/start_ckan.sh +++ b/rootfs/setup/app/start_ckan.sh @@ -1,7 +1,4 @@ #!/bin/bash -# Run the prerun script to init CKAN and create the default admin user -python prerun.py - # Run any startup scripts provided by images extending this one if [[ -d "/docker-entrypoint.d" ]] then @@ -15,6 +12,9 @@ then done fi +# Run the prerun script to init CKAN and create the default admin user +python prerun.py + # Set the common uwsgi options UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L" From 9829cafc86e7734fec6433e93683cb5bb43fc715 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Tue, 9 Jun 2020 13:33:39 +0200 Subject: [PATCH 041/213] Fixes urllib2 issues Adds s3filestore requirements in Dockerfile --- rootfs/Dockerfile | 4 +++- rootfs/setup/app/prerun.py | 27 +++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index e8d32b2..2c08e96 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -82,7 +82,8 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars RUN pip wheel --wheel-dir=/wheels git+${S3FILESTORE_GIT_URL}@${S3FILESTORE_GIT_BRANCH}#egg=ckanext-s3filestore - +RUN pip wheel --wheel-dir=/wheels -r https://raw.githubusercontent.com/okfn/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt +RUN curl -o /wheels/s3filestore.txt https://raw.githubusercontent.com/okfn/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt ############ ### MAIN ### @@ -138,6 +139,7 @@ RUN pip install -e /srv/app/src/ckan && \ pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ # Install default CKAN extensions pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars ckanext-s3filestore && \ + pip install --no-index --find-links=/srv/app/ext_wheels -r /srv/app/ext_wheels/s3filestore.txt && \ # Create and update CKAN config # Set timezone echo "UTC" > /etc/timezone && \ diff --git a/rootfs/setup/app/prerun.py b/rootfs/setup/app/prerun.py index 5f55f70..587e534 100644 --- a/rootfs/setup/app/prerun.py +++ b/rootfs/setup/app/prerun.py @@ -2,7 +2,7 @@ import os import sys import subprocess import psycopg2 -import urllib.request, urllib.error, urllib.parse +import urllib2 import re import time @@ -48,8 +48,8 @@ def check_solr_connection(retry=None): search_url = '{url}/select/?q=*&wt=json'.format(url=url) try: - connection = urllib.request.urlopen(search_url) - except urllib.error.URLError as e: + connection = urllib2.urlopen(search_url) + except urllib2.URLError as e: print((str(e))) print('[prerun] Unable to connect to solr...try again in a while.') import time @@ -58,15 +58,16 @@ def check_solr_connection(retry=None): else: import re conn_info = connection.read() + conn_info = re.sub(r'"zkConnected":true', '"zkConnected":True', conn_info) eval(conn_info) def init_db(): print('[prerun] Start init_db...') - db_command = ['ckan', '-c', ckan_ini, 'db', 'init'] + db_command = ['paster', '--plugin=ckan', 'db', 'init', '-c', ckan_ini] - print('[prerun] Initializing or upgrading db - start using ckan db init') + print('[prerun] Initializing or upgrading db - start using paster db init') try: # run init scripts subprocess.check_output(db_command, stderr=subprocess.STDOUT) @@ -92,8 +93,8 @@ def init_datastore(): print('[prerun] Skipping datastore initialization') return - datastore_perms_command = ['ckan', '-c', ckan_ini, 'datastore', - 'set-permissions'] + datastore_perms_command = ['paster', '--plugin=ckan', 'datastore', + 'set-permissions', '-c', ckan_ini] connection = psycopg2.connect(conn_str) cursor = connection.cursor() @@ -144,7 +145,7 @@ def create_sysadmin(): if name and password and email: # Check if user exists - command = ['ckan', '-c', ckan_ini, 'user', 'show', name] + command = ['paster', '--plugin=ckan', 'user', name, '-c', ckan_ini] out = subprocess.check_output(command) if 'User:None' not in re.sub(r'\s', '', out.decode('utf-8')): @@ -152,17 +153,19 @@ def create_sysadmin(): return # Create user - command = ['ckan', '-c', ckan_ini, 'user', 'add', + command = ['paster', '--plugin=ckan', 'user', 'add', name, 'password=' + password, - 'email=' + email] + 'email=' + email, + '-c', ckan_ini] subprocess.call(command) print(('[prerun] Created user {0}'.format(name))) # Make it sysadmin - command = ['ckan', '-c', ckan_ini, 'sysadmin', 'add', - name] + command = ['paster', '--plugin=ckan', 'sysadmin', 'add', + name, + '-c', ckan_ini] subprocess.call(command) print(('[prerun] Made user {0} a sysadmin'.format(name))) From 5b2f5cef9a5222a7d712311a980b01f6d7196d98 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Thu, 11 Jun 2020 10:30:11 +0200 Subject: [PATCH 042/213] Use gevent early monkey patch due to upstream issues --- rootfs/setup/app/start_ckan.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh index 8f71c90..a3798ff 100755 --- a/rootfs/setup/app/start_ckan.sh +++ b/rootfs/setup/app/start_ckan.sh @@ -16,7 +16,7 @@ fi python prerun.py # Set the common uwsgi options -UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L" +UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L --gevent-early-monkey-patch" # Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully if [ $? -eq 0 ] From e865d2c89d32c9cd8a2099429325d59ddfbe35e3 Mon Sep 17 00:00:00 2001 From: Dragan Bocevski Date: Wed, 17 Jun 2020 15:01:29 +0200 Subject: [PATCH 043/213] The if condition was always true, so failed prerun.py was not handled properly --- rootfs/setup/app/start_ckan.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh index a3798ff..f7773f5 100755 --- a/rootfs/setup/app/start_ckan.sh +++ b/rootfs/setup/app/start_ckan.sh @@ -12,12 +12,12 @@ then done fi -# Run the prerun script to init CKAN and create the default admin user -python prerun.py - # Set the common uwsgi options UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L --gevent-early-monkey-patch" +# Run the prerun script to init CKAN and create the default admin user +python prerun.py + # Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully if [ $? -eq 0 ] then From ae70de6fdd0ff5804760f8afc17aa754beb03ea6 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Fri, 21 Aug 2020 12:18:23 +0200 Subject: [PATCH 044/213] fixes wrong location check and call for the docker entrypoint --- rootfs/setup/app/start_ckan.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh index a3798ff..d7656ea 100755 --- a/rootfs/setup/app/start_ckan.sh +++ b/rootfs/setup/app/start_ckan.sh @@ -1,8 +1,8 @@ #!/bin/bash # Run any startup scripts provided by images extending this one -if [[ -d "/docker-entrypoint.d" ]] +if [[ -d "${APP_DIR}/docker-entrypoint.d" ]] then - for f in /docker-entrypoint.d/*; do + for f in ${APP_DIR}/docker-entrypoint.d/*; do case "$f" in *.sh) echo "$0: Running init file $f"; . "$f" ;; *.py) echo "$0: Running init file $f"; python "$f"; echo ;; From a1d25d05a53a7aa46782421ac8b56e95e11ae4a4 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Fri, 21 Aug 2020 14:59:40 +0200 Subject: [PATCH 045/213] changes okfn s3filestore extension for the keitaro one --- rootfs/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile index 2c08e96..9c781a9 100644 --- a/rootfs/Dockerfile +++ b/rootfs/Dockerfile @@ -63,8 +63,8 @@ ENV DEFAULT_EXTENSIONS envvars s3filestore # Locations and tags, please use specific tags or revisions ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars ENV ENVVARS_GIT_BRANCH=0.0.1 -ENV S3FILESTORE_GIT_URL=https://github.com/okfn/ckanext-s3filestore -ENV S3FILESTORE_GIT_BRANCH=33d4b60 +ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore +ENV S3FILESTORE_GIT_BRANCH=efd5711 RUN apk add --no-cache \ git \ @@ -82,8 +82,8 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars RUN pip wheel --wheel-dir=/wheels git+${S3FILESTORE_GIT_URL}@${S3FILESTORE_GIT_BRANCH}#egg=ckanext-s3filestore -RUN pip wheel --wheel-dir=/wheels -r https://raw.githubusercontent.com/okfn/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt -RUN curl -o /wheels/s3filestore.txt https://raw.githubusercontent.com/okfn/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt +RUN pip wheel --wheel-dir=/wheels -r https://raw.githubusercontent.com/keitaroinc/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt +RUN curl -o /wheels/s3filestore.txt https://raw.githubusercontent.com/keitaroinc/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt ############ ### MAIN ### From 27af03db6fa868dc745216f45608d125225c0111 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Fri, 4 Sep 2020 14:15:14 +0200 Subject: [PATCH 046/213] adds logging arguments to uwsgi options --- rootfs/setup/app/start_ckan.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rootfs/setup/app/start_ckan.sh b/rootfs/setup/app/start_ckan.sh index 7cbc936..f870cb8 100755 --- a/rootfs/setup/app/start_ckan.sh +++ b/rootfs/setup/app/start_ckan.sh @@ -13,7 +13,7 @@ then fi # Set the common uwsgi options -UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L --gevent-early-monkey-patch" +UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --paste-logger /srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L --gevent-early-monkey-patch" # Run the prerun script to init CKAN and create the default admin user python prerun.py From 2410c275630f28667614d07df307b3292235d1a3 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 11 Sep 2020 11:28:34 +0200 Subject: [PATCH 047/213] Restructure the layout and add compose and examples --- Readme.md | 121 +++--- compose/.ckan-env | 33 ++ compose/.env | 29 ++ compose/docker-compose-minio.yml | 101 +++++ compose/docker-compose.yml | 88 +++++ compose/postgresql/Dockerfile | 13 + .../00_create_datastore.sh | 8 + .../20_postgis_permissions.sql | 3 + compose/solr/Dockerfile | 34 ++ compose/solr/solrconfig-2.8.5.xml | 343 +++++++++++++++++ compose/solr/solrconfig-2.9.0.xml | 345 ++++++++++++++++++ examples/Dockerfile | 52 +++ images/ckan/2.7/Dockerfile | 176 +++++++++ .../ckan/2.7}/setup/app/extra_scripts.sh | 0 .../ckan/2.7}/setup/app/prerun.py | 4 - .../ckan/2.7}/setup/app/start_ckan.sh | 0 .../ckan/2.7}/setup/app/uwsgi.conf | 0 {rootfs => images/ckan/2.8}/Dockerfile | 9 +- images/ckan/2.8/setup/app/extra_scripts.sh | 4 + images/ckan/2.8/setup/app/prerun.py | 182 +++++++++ images/ckan/2.8/setup/app/start_ckan.sh | 42 +++ images/ckan/2.8/setup/app/uwsgi.conf | 2 + images/ckan/2.9/Dockerfile | 179 +++++++++ images/ckan/2.9/setup/app/extra_scripts.sh | 4 + images/ckan/2.9/setup/app/prerun.py | 182 +++++++++ images/ckan/2.9/setup/app/start_ckan.sh | 42 +++ images/ckan/2.9/setup/app/uwsgi.conf | 2 + images/ckan/2.9/setup/app/wsgi.py | 12 + images/datapusher/Dockerfile | 100 +++++ .../datapusher/setup/datapusher_settings.py | 33 ++ images/datapusher/setup/wsgi.py | 9 + 31 files changed, 2102 insertions(+), 50 deletions(-) create mode 100644 compose/.ckan-env create mode 100644 compose/.env create mode 100644 compose/docker-compose-minio.yml create mode 100644 compose/docker-compose.yml create mode 100644 compose/postgresql/Dockerfile create mode 100644 compose/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh create mode 100644 compose/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql create mode 100644 compose/solr/Dockerfile create mode 100644 compose/solr/solrconfig-2.8.5.xml create mode 100644 compose/solr/solrconfig-2.9.0.xml create mode 100644 examples/Dockerfile create mode 100644 images/ckan/2.7/Dockerfile rename {rootfs => images/ckan/2.7}/setup/app/extra_scripts.sh (100%) rename {rootfs => images/ckan/2.7}/setup/app/prerun.py (95%) rename {rootfs => images/ckan/2.7}/setup/app/start_ckan.sh (100%) rename {rootfs => images/ckan/2.7}/setup/app/uwsgi.conf (100%) rename {rootfs => images/ckan/2.8}/Dockerfile (95%) create mode 100755 images/ckan/2.8/setup/app/extra_scripts.sh create mode 100644 images/ckan/2.8/setup/app/prerun.py create mode 100755 images/ckan/2.8/setup/app/start_ckan.sh create mode 100644 images/ckan/2.8/setup/app/uwsgi.conf create mode 100644 images/ckan/2.9/Dockerfile create mode 100755 images/ckan/2.9/setup/app/extra_scripts.sh create mode 100644 images/ckan/2.9/setup/app/prerun.py create mode 100755 images/ckan/2.9/setup/app/start_ckan.sh create mode 100644 images/ckan/2.9/setup/app/uwsgi.conf create mode 100644 images/ckan/2.9/setup/app/wsgi.py create mode 100644 images/datapusher/Dockerfile create mode 100644 images/datapusher/setup/datapusher_settings.py create mode 100644 images/datapusher/setup/wsgi.py diff --git a/Readme.md b/Readme.md index 1e58126..584ce2a 100644 --- a/Readme.md +++ b/Readme.md @@ -1,58 +1,93 @@ -# Docker ckan image ![Docker Pulls](https://img.shields.io/docker/pulls/keitaro/ckan.svg) +# Dockerized CKAN ![Docker Pulls](https://img.shields.io/docker/pulls/keitaro/ckan.svg) +This repository contains base docker images, examples and docker-compose used to build and run CKAN. + +We build and publish docker images built using this repository to Dockerhub: +- [CKAN docker images](https://hub.docker.com/r/keitaro/ckan). +- [Datapusher docker images](https://hub.docker.com/r/keitaro/ckan-datapusher) + +Looking to run CKAN on Kubernetes? Check out our [CKAN Helm Chart](https://github.com/keitaroinc/ckan-helm)! ## Overview +All images are based on [Alpine Linux](https://alpinelinux.org/) and include only required extensions to start a CKAN instance. The docker images are built using a multi-stage docker approach in order to produce slim production grade docker images with the right libraries and configuration. This multi-stage approach allows us to build python binary wheels in the build stages that later on we install in the main stage. -This repository contains base docker image used to build CKAN instances. It's based on [Alpine Linux](https://alpinelinux.org/) and includes only required extensions to start CKAN instance. +Directory layout: +- [compose](./compose) - contains a docker-compose setup allowing users to spin up a CKAN setup easily using [docker-compose](https://docs.docker.com/compose/) +- [images](./images) - includes docker contexts for building all supported CKAN versions and datapusher +- [examples](./examples) - includes examples on how to extend the CKAN docker images and how to run them + +## Running CKAN using docker-compose +To start CKAN using docker-compose, simply change into the *compose* directory and run +```sh +cd compose +docker-compose build +docker-compose up +``` +Check if CKAN was succesfuly started on http://localhost:5000. + +### Configuration +In order to configure CKAN within docker-compose we use both build/up time variables loaded via the [.env](./compose/.env) file, and runtime variables loaded via the [.ckan-env](./compose/.ckan-env) file. + +Variables in the [.env](./compose/.env) file are loaded when running `docker-compose build` and `docker-compose up`, while variables in [.ckan-env](./compose/.ckan-env) file are used withing the CKAN container at runtime to configure CKAN and CKAN extensions using [ckanext-envvars](https://github.com/okfn/ckanext-envvars). + +## Extending CKAN docker images +Check some examples of extending CKAN docker images in the [examples](./examples) directory. + +We recommend to use a multi-stage approach to extend the docker images that we provide here. To extend the images the following Dockerfile structure is recommended: +```docker +################### +### Extensions #### +################### +FROM keitaro/ckan:2.9.0 as extbuild + +# Switch to the root user +USER root + +# Install any system packages necessary to build extensions +RUN apk add --no-cache python3-dev + +# Fetch and build the custom CKAN extensions +RUN pip wheel --wheel-dir=/wheels git+https://github.com/acmecorp/ckanext-acme@0.0.1#egg=ckanext-acme + +############ +### MAIN ### +############ +FROM keitaro/ckan:2.9.0 + +# Add the custom extensions to the plugins list +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher acme + +# Switch to the root user +USER root + +COPY --from=extbuild /wheels /srv/app/ext_wheels + +# Install and enable the custom extensions +RUN pip install --no-index --find-links=/srv/app/ext_wheels ckanext-acme && \ + ckan -c ${APP_DIR}/production.ini config-tool "ckan.plugins = ${CKAN__PLUGINS}" && \ + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/ext_wheels + +# Switch to the ckan user +USER ckan +``` + +### Adding prerun scripts +You can add scripts to CKAN custom images and copy them to the *docker-entrypoint.d* directory. Any *.sh or *.py file in that directory will be executed after the main initialization script (prerun.py) is executed. ## Build - -To create new image run: - +To build a CKAN image run: ```sh -docker build --tag ckan-2.8.2 . +docker build --tag keitaro/ckan:2.9.0 images/ckan/2.9 ``` -The –-tag ckan-2.8.2 flag sets the image name to ckan-2.8.2 and the dot ( “.” ) at the end tells docker build to look into the current directory for Dockerfile and related contents. - -## List - -Check if the image shows up in the list of images: -```sh - docker images -``` - -## Run - -To start and test newly created image run: -```sh - docker run ckan-2.8.2 -``` -Check if CKAN was succesfuly started on http://localhost:5000. The ckan site url is configured in ENV CKAN_SITE_URL. - +The –-tag keitaro/ckan:2.9.0 flag sets the image name to ketiaro/ckan:2.9.0 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. ## Upload to DockerHub - >*It's recommended to upload built images to DockerHub* To upload the image to DockerHub run: ```sh -docker push [options] /ckan: +docker push [options] /ckan: ``` - -## Upgrade -To upgrade the Docker file to use new CKAN version, in the Dockerfile you should change: - ->ENV GIT_BRANCH={ckan_release} - -Check [CKAN repository](https://github.com/ckan/ckan/releases) for the latest releases. -If there are new libraries used by the new version requirements, those needs to be included too. - -## Extensions - -Default extensions used in the Dockerfile are kept in: - ->ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher - -## Add new scripts - -You can add scripts to CKAN custom images and copy them to the *docker-entrypoint.d* directory. Any *.sh or *.py file in that directory will be executed after the main initialization script (prerun.py) is executed. diff --git a/compose/.ckan-env b/compose/.ckan-env new file mode 100644 index 0000000..cb42e24 --- /dev/null +++ b/compose/.ckan-env @@ -0,0 +1,33 @@ +# Runtime configuration of CKAN enabled through ckanext-envvars +# Information about how it works: https://github.com/okfn/ckanext-envvars +# Note that variables here take presedence over build/up time variables in .env + +# General Settings +CKAN_SITE_ID=default +CKAN_SITE_URL=http://localhost:5000 +CKAN_PORT=5000 +CKAN_MAX_UPLOAD_SIZE_MB=10 +# CKAN Plugins +CKAN__PLUGINS=envvars image_view text_view recline_view datastore datapusher +# CKAN requires storage path to be set in order for filestore to be enabled +CKAN__STORAGE_PATH=/srv/app/data +CKAN__WEBASSETS__PATH=/srv/app/data/webassets +# SYSADMIN settings, a sysadmin user is created automatically with the below credentials +CKAN_SYSADMIN_NAME=sysadmin +CKAN_SYSADMIN_PASSWORD=password +CKAN_SYSADMIN_EMAIL=sysadmin@ckantest.com + +# Email settings +CKAN_SMTP_SERVER=smtp.corporateict.domain:25 +CKAN_SMTP_STARTTLS=True +CKAN_SMTP_USER=user +CKAN_SMTP_PASSWORD=pass +CKAN_SMTP_MAIL_FROM=ckan@localhost + +# S3/MINIO settings +#CKANEXT__S3FILESTORE__AWS_ACCESS_KEY_ID = MINIOACCESSKEY +#CKANEXT__S3FILESTORE__AWS_SECRET_ACCESS_KEY = MINIOSECRETKEY +#CKANEXT__S3FILESTORE__AWS_BUCKET_NAME = ckan +#CKANEXT__S3FILESTORE__HOST_NAME = http://minio:9000 +#CKANEXT__S3FILESTORE__REGION_NAME = us-east-1 +#CKANEXT__S3FILESTORE__SIGNATURE_VERSION = s3v4 diff --git a/compose/.env b/compose/.env new file mode 100644 index 0000000..89b30ec --- /dev/null +++ b/compose/.env @@ -0,0 +1,29 @@ +# Variables in this file will be used as build arguments when running +# docker-compose build and docker-compose up +# Verify correct substitution with "docker-compose config" +# If variables are newly added or enabled, please delete and rebuild the images to pull in changes: +# docker-compose down -v +# docker-compose build +# docker-compose up -d + +# Database +POSTGRES_PASSWORD=ckan +POSTGRES_PORT=5432 +DATASTORE_READONLY_PASSWORD=datastore + +# CKAN +CKAN_VERSION=2.9.0 +CKAN_SITE_ID=default +CKAN_SITE_URL=http://localhost:5000 +CKAN_PORT=5000 +CKAN_MAX_UPLOAD_SIZE_MB=10 + +# Datapusher +DATAPUSHER_VERSION=0.0.17 +DATAPUSHER_MAX_CONTENT_LENGTH=10485760 +DATAPUSHER_CHUNK_SIZE=16384 +DATAPUSHER_CHUNK_INSERT_ROWS=250 +DATAPUSHER_DOWNLOAD_TIMEOUT=30 + +# Redis +REDIS_VERSION=6.0.7 diff --git a/compose/docker-compose-minio.yml b/compose/docker-compose-minio.yml new file mode 100644 index 0000000..e12a0ab --- /dev/null +++ b/compose/docker-compose-minio.yml @@ -0,0 +1,101 @@ +# docker-compose build && docker-compose up -d +version: "3" + +volumes: + minio_data: + pg_data: + solr_data: + +services: + ckan: + container_name: ckan + image: keitaro/ckan:${CKAN_VERSION} + links: + - db + - solr + - redis + - minio + depends_on: + - db + ports: + - "0.0.0.0:${CKAN_PORT}:5000" + env_file: + - ./.ckan-env + environment: + - CKAN_SQLALCHEMY_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/ckan + - CKAN_DATASTORE_WRITE_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/datastore + - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore + - CKAN_SOLR_URL=http://solr:8983/solr/ckan + - CKAN_REDIS_URL=redis://redis:6379/1 + - CKAN_DATAPUSHER_URL=http://datapusher:8000 + - CKAN_SITE_URL=${CKAN_SITE_URL} + - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + + datapusher: + container_name: datapusher + image: keitaro/ckan-datapusher:${DATAPUSHER_VERSION} + links: + - ckan + ports: + - "8000:8000" + environment: + - DATAPUSHER_MAX_CONTENT_LENGTH=${DATAPUSHER_MAX_CONTENT_LENGTH} + - DATAPUSHER_CHUNK_SIZE=${DATAPUSHER_CHUNK_SIZE} + - DATAPUSHER_CHUNK_INSERT_ROWS=${DATAPUSHER_CHUNK_INSERT_ROWS} + - DATAPUSHER_DOWNLOAD_TIMEOUT=${DATAPUSHER_DOWNLOAD_TIMEOUT} + + db: + container_name: db + build: + context: . + dockerfile: postgresql/Dockerfile + args: + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + environment: + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + volumes: + - pg_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "ckan"] + + solr: + container_name: solr + build: + context: . + dockerfile: solr/Dockerfile + args: + - CKAN_VERSION=${CKAN_VERSION} + volumes: + - solr_data:/opt/solr/server/solr/ckan/data + + redis: + container_name: redis + image: redis:${REDIS_VERSION} + + minio: + container_name: minio + image: minio/minio:RELEASE.2020-08-08T04-50-06Z + ports: + - "0.0.0.0:9000:9000" + environment: + - MINIO_ACCESS_KEY=MINIOACCESSKEY + - MINIO_SECRET_KEY=MINIOSECRETKEY + volumes: + - minio_data:/data + command: server /data + + mc: + container_name: mc + image: minio/mc:RELEASE.2020-08-08T02-33-58Z + depends_on: + - minio + entrypoint: > + /bin/sh -c " + /usr/bin/mc config host rm local; + /usr/bin/mc config host add --quiet --api s3v4 local http://minio:9000 minio ckan123; + /usr/bin/mc mb --quiet local/ckan/; + " diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml new file mode 100644 index 0000000..f8f48c4 --- /dev/null +++ b/compose/docker-compose.yml @@ -0,0 +1,88 @@ +# docker-compose build && docker-compose up -d +version: "3" + +volumes: + ckan_data: + pg_data: + solr_data: + +services: + ckan: + container_name: ckan + image: keitaro/ckan:${CKAN_VERSION} + networks: + - frontend + - backend + depends_on: + - db + ports: + - "0.0.0.0:${CKAN_PORT}:5000" + env_file: + - ./.ckan-env + environment: + - CKAN_SQLALCHEMY_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/ckan + - CKAN_DATASTORE_WRITE_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/datastore + - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore + - CKAN_SOLR_URL=http://solr:8983/solr/ckan + - CKAN_REDIS_URL=redis://redis:6379/1 + - CKAN_DATAPUSHER_URL=http://datapusher:8000 + - CKAN_SITE_URL=${CKAN_SITE_URL} + - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + volumes: + - ckan_data:/srv/app/data + + datapusher: + container_name: datapusher + image: keitaro/ckan-datapusher:${DATAPUSHER_VERSION} + networks: + - frontend + - backend + ports: + - "8000:8000" + environment: + - DATAPUSHER_MAX_CONTENT_LENGTH=${DATAPUSHER_MAX_CONTENT_LENGTH} + - DATAPUSHER_CHUNK_SIZE=${DATAPUSHER_CHUNK_SIZE} + - DATAPUSHER_CHUNK_INSERT_ROWS=${DATAPUSHER_CHUNK_INSERT_ROWS} + - DATAPUSHER_DOWNLOAD_TIMEOUT=${DATAPUSHER_DOWNLOAD_TIMEOUT} + + db: + container_name: db + build: + context: . + dockerfile: postgresql/Dockerfile + args: + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + networks: + - backend + environment: + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + volumes: + - pg_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "ckan"] + + solr: + container_name: solr + build: + context: . + dockerfile: solr/Dockerfile + args: + - CKAN_VERSION=${CKAN_VERSION} + networks: + - backend + volumes: + - solr_data:/opt/solr/server/solr/ckan/data + + redis: + container_name: redis + image: redis:${REDIS_VERSION} + networks: + - backend + +networks: + frontend: + backend: diff --git a/compose/postgresql/Dockerfile b/compose/postgresql/Dockerfile new file mode 100644 index 0000000..f7e9795 --- /dev/null +++ b/compose/postgresql/Dockerfile @@ -0,0 +1,13 @@ +FROM mdillon/postgis:11 + +# Allow connections; we don't map out any ports so only linked docker containers can connect +RUN echo "host all all 0.0.0.0/0 md5" >> /var/lib/postgresql/data/pg_hba.conf + +# Customize default user/pass/db +ENV POSTGRES_DB ckan +ENV POSTGRES_USER ckan +ARG POSTGRES_PASSWORD +ARG DS_RO_PASS + +# Include datastore setup scripts +ADD ./postgresql/docker-entrypoint-initdb.d /docker-entrypoint-initdb.d diff --git a/compose/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh b/compose/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh new file mode 100644 index 0000000..ec1b3c3 --- /dev/null +++ b/compose/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + CREATE ROLE datastore_ro NOSUPERUSER NOCREATEDB NOCREATEROLE LOGIN PASSWORD '$DS_RO_PASS'; + CREATE DATABASE datastore OWNER ckan ENCODING 'utf-8'; + GRANT ALL PRIVILEGES ON DATABASE datastore TO ckan; +EOSQL \ No newline at end of file diff --git a/compose/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql b/compose/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql new file mode 100644 index 0000000..0eb2f85 --- /dev/null +++ b/compose/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION POSTGIS; +ALTER VIEW geometry_columns OWNER TO ckan; +ALTER TABLE spatial_ref_sys OWNER TO ckan; diff --git a/compose/solr/Dockerfile b/compose/solr/Dockerfile new file mode 100644 index 0000000..f49dd33 --- /dev/null +++ b/compose/solr/Dockerfile @@ -0,0 +1,34 @@ +FROM solr:6.6.6 + +# Enviroment +ENV SOLR_CORE ckan +ENV SOLR_VERSION 6.6.6 + +# Build Arguments +ARG CKAN_VERSION + +# Create Directories +RUN mkdir -p /opt/solr/server/solr/$SOLR_CORE/conf +RUN mkdir -p /opt/solr/server/solr/$SOLR_CORE/data + +# Adding Files +ADD ./solr/solrconfig-$CKAN_VERSION.xml \ +https://raw.githubusercontent.com/ckan/ckan/ckan-$CKAN_VERSION/ckan/config/solr/schema.xml \ +https://raw.githubusercontent.com/apache/lucene-solr/releases/lucene-solr/$SOLR_VERSION/solr/server/solr/configsets/basic_configs/conf/currency.xml \ +https://raw.githubusercontent.com/apache/lucene-solr/releases/lucene-solr/$SOLR_VERSION/solr/server/solr/configsets/basic_configs/conf/synonyms.txt \ +https://raw.githubusercontent.com/apache/lucene-solr/releases/lucene-solr/$SOLR_VERSION/solr/server/solr/configsets/basic_configs/conf/stopwords.txt \ +https://raw.githubusercontent.com/apache/lucene-solr/releases/lucene-solr/$SOLR_VERSION/solr/server/solr/configsets/basic_configs/conf/protwords.txt \ +https://raw.githubusercontent.com/apache/lucene-solr/releases/lucene-solr/$SOLR_VERSION/solr/server/solr/configsets/data_driven_schema_configs/conf/elevate.xml \ +/opt/solr/server/solr/$SOLR_CORE/conf/ + +# Create Core.properties +RUN mv /opt/solr/server/solr/$SOLR_CORE/conf/solrconfig-$CKAN_VERSION.xml /opt/solr/server/solr/$SOLR_CORE/conf/solrconfig.xml && \ + echo name=$SOLR_CORE > /opt/solr/server/solr/$SOLR_CORE/core.properties + +# Giving ownership to Solr + +USER root +RUN chown -R $SOLR_USER:$SOLR_USER /opt/solr/server/solr/$SOLR_CORE + +# User +USER $SOLR_USER:$SOLR_USER diff --git a/compose/solr/solrconfig-2.8.5.xml b/compose/solr/solrconfig-2.8.5.xml new file mode 100644 index 0000000..9ac620c --- /dev/null +++ b/compose/solr/solrconfig-2.8.5.xml @@ -0,0 +1,343 @@ + + + + + + 6.0.0 + + + + + + + + + + + ${solr.data.dir:} + + + + + + + ${solr.lock.type:native} + + + + + + + + ${solr.ulog.dir:} + ${solr.ulog.numVersionBuckets:65536} + + + + ${solr.autoCommit.maxTime:15000} + false + + + + ${solr.autoSoftCommit.maxTime:-1} + + + + + + + 1024 + + + + + true + 20 + 200 + + + + + + + + + + false + 2 + + + + + + + + + + + + + + explicit + 10 + + + + + + + + explicit + json + true + + + + + + + + explicit + + + + + + + + _text_ + + + + + + + + add-unknown-fields-to-the-schema + + + + + + + + true + ignored_ + _text_ + + + + + + + + + + + explicit + true + + + + + + + text_general + + + default + _text_ + solr.DirectSolrSpellChecker + internal + 0.5 + 2 + 1 + 5 + 4 + 0.01 + + + + + + + + default + on + true + 10 + 5 + 5 + true + true + 10 + 5 + + + + spellcheck + + + + + + + + + + true + + + tvComponent + + + + + + + + + + true + false + + + + terms + + + + + + + string + elevate.xml + + + + + + + explicit + + + elevator + + + + + + + + + + + 100 + + + + + + 70 + 0.5 + [-\w ,/\n\"']{20,200} + + + + + + ]]> + ]]> + + + + + + + + + + + + ,, + ,, + ,, + ,, + ,]]> + ]]> + + + + + + + 10 + .,!? + + + + + + WORD + en + US + + + + + + + + + + + + + + + + [^\w-\.] + _ + + + + + + + yyyy-MM-dd'T'HH:mm:ss.SSSZ + yyyy-MM-dd'T'HH:mm:ss,SSSZ + yyyy-MM-dd'T'HH:mm:ss.SSS + yyyy-MM-dd'T'HH:mm:ss,SSS + yyyy-MM-dd'T'HH:mm:ssZ + yyyy-MM-dd'T'HH:mm:ss + yyyy-MM-dd'T'HH:mmZ + yyyy-MM-dd'T'HH:mm + yyyy-MM-dd HH:mm:ss.SSSZ + yyyy-MM-dd HH:mm:ss,SSSZ + yyyy-MM-dd HH:mm:ss.SSS + yyyy-MM-dd HH:mm:ss,SSS + yyyy-MM-dd HH:mm:ssZ + yyyy-MM-dd HH:mm:ss + yyyy-MM-dd HH:mmZ + yyyy-MM-dd HH:mm + yyyy-MM-dd + + + + + + + text/plain; charset=UTF-8 + + + + ${velocity.template.base.dir:} + ${velocity.solr.resource.loader.enabled:true} + ${velocity.params.resource.loader.enabled:false} + + + + 5 + + + diff --git a/compose/solr/solrconfig-2.9.0.xml b/compose/solr/solrconfig-2.9.0.xml new file mode 100644 index 0000000..8a3eade --- /dev/null +++ b/compose/solr/solrconfig-2.9.0.xml @@ -0,0 +1,345 @@ + + + + + + 6.0.0 + + + + + + + + + + + ${solr.data.dir:} + + + + + + + ${solr.lock.type:native} + + + + + + + + ${solr.ulog.dir:} + ${solr.ulog.numVersionBuckets:65536} + + + + ${solr.autoCommit.maxTime:15000} + false + + + + ${solr.autoSoftCommit.maxTime:-1} + + + + + + + 1024 + + + + + true + 20 + 200 + + + + + + + + + + false + 2 + + + + + + + + + + + + + + explicit + 10 + + + + + + + + explicit + json + true + + + + + + + + explicit + + + + + + + + _text_ + + + + + + + + add-unknown-fields-to-the-schema + + + + + + + + true + ignored_ + _text_ + + + + + + + + + + + explicit + true + + + + + + + text_general + + + default + _text_ + solr.DirectSolrSpellChecker + internal + 0.5 + 2 + 1 + 5 + 4 + 0.01 + + + + + + + + default + on + true + 10 + 5 + 5 + true + true + 10 + 5 + + + + spellcheck + + + + + + + + + + true + + + tvComponent + + + + + + + + + + true + false + + + + terms + + + + + + + string + elevate.xml + + + + + + + explicit + + + elevator + + + + + + + + + + + 100 + + + + + + 70 + 0.5 + [-\w ,/\n\"']{20,200} + + + + + + ]]> + ]]> + + + + + + + + + + + + ,, + ,, + ,, + ,, + ,]]> + ]]> + + + + + + + 10 + .,!? + + + + + + WORD + en + US + + + + + + + + + true + + + + + + + + + [^\w-\.] + _ + + + + + + + yyyy-MM-dd'T'HH:mm:ss.SSSZ + yyyy-MM-dd'T'HH:mm:ss,SSSZ + yyyy-MM-dd'T'HH:mm:ss.SSS + yyyy-MM-dd'T'HH:mm:ss,SSS + yyyy-MM-dd'T'HH:mm:ssZ + yyyy-MM-dd'T'HH:mm:ss + yyyy-MM-dd'T'HH:mmZ + yyyy-MM-dd'T'HH:mm + yyyy-MM-dd HH:mm:ss.SSSZ + yyyy-MM-dd HH:mm:ss,SSSZ + yyyy-MM-dd HH:mm:ss.SSS + yyyy-MM-dd HH:mm:ss,SSS + yyyy-MM-dd HH:mm:ssZ + yyyy-MM-dd HH:mm:ss + yyyy-MM-dd HH:mmZ + yyyy-MM-dd HH:mm + yyyy-MM-dd + + + + + + + text/plain; charset=UTF-8 + + + + ${velocity.template.base.dir:} + ${velocity.solr.resource.loader.enabled:true} + ${velocity.params.resource.loader.enabled:false} + + + + 5 + + + diff --git a/examples/Dockerfile b/examples/Dockerfile new file mode 100644 index 0000000..eee81a3 --- /dev/null +++ b/examples/Dockerfile @@ -0,0 +1,52 @@ +################### +### Extensions #### +################### +FROM keitaro/ckan:2.9.0 as extbuild + +MAINTAINER Keitaro Inc + +# Locations and tags, please use specific tags or revisions +ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest +ENV HARVEST_GIT_BRANCH=v1.3.1 + +# Switch to the root user +USER root + +# Install necessary packages to build extensions +RUN apk add --no-cache \ + gcc \ + g++ \ + libffi-dev \ + openssl-dev \ + python3-dev + +# Fetch and build the custom CKAN extensions +RUN pip wheel --wheel-dir=/wheels git+${HARVEST_GIT_URL}@${HARVEST_GIT_BRANCH}#egg=ckanext-harvest +RUN pip wheel --wheel-dir=/wheels -r https://raw.githubusercontent.com/ckan/ckanext-harvest/${HARVEST_GIT_BRANCH}/pip-requirements.txt +RUN curl -o /wheels/harvest.txt https://raw.githubusercontent.com/ckan/ckanext-harvest/${HARVEST_GIT_BRANCH}/pip-requirements.txt + +############ +### MAIN ### +############ +FROM keitaro/ckan:2.9.0 + +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher harvest ckan_harvester + +# Switch to the root user +USER root + +COPY --from=extbuild /wheels /srv/app/ext_wheels + +# Install and enable the custom extensions +RUN pip install --no-index --find-links=/srv/app/ext_wheels ckanext-harvest && \ + pip install --no-index --find-links=/srv/app/ext_wheels -r /srv/app/ext_wheels/harvest.txt && \ + # Not working atm since ckan config tool tries to load config before executing config-tool, workaround + # ckan -c ${APP_DIR}/production.ini config-tool "ckan.plugins = ${CKAN__PLUGINS}" && \ + sed -i "/ckan.plugins = envvars/c ckan.plugins = ${CKAN__PLUGINS}" ${APP_DIR}/production.ini && \ + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/ext_wheels + +# Switch to the ckan user +USER ckan diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile new file mode 100644 index 0000000..1a4c72e --- /dev/null +++ b/images/ckan/2.7/Dockerfile @@ -0,0 +1,176 @@ +################## +### Build CKAN ### +################## +FROM alpine:3.12 as ckanbuild + +# Set CKAN version to build +ENV GIT_URL=https://github.com/ckan/ckan.git +ENV GIT_BRANCH=ckan-2.7.8 + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +WORKDIR ${SRC_DIR} + +# Packages to build CKAN requirements and plugins +RUN apk add --no-cache \ + git \ + curl \ + python2 \ + postgresql-dev \ + linux-headers \ + gcc \ + make \ + g++ \ + autoconf \ + automake \ + libtool \ + musl-dev \ + pcre-dev \ + pcre \ + python2-dev \ + libffi-dev \ + libxml2-dev \ + libxslt-dev + +# Install version 9.x of postgresql-dev so that CKAN requirements can be built +RUN apk add --repository http://dl-cdn.alpinelinux.org/alpine/v3.6/main \ + postgresql-dev~=9.6 + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Fetch and build CKAN and requirements +RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan +RUN rm -rf /srv/app/src/ckan/.git +RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 + + +########################### +### Default-Extensions #### +########################### +FROM alpine:3.12 as extbuild + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +# List of default extensions +ENV DEFAULT_EXTENSIONS envvars s3filestore + +# Locations and tags, please use specific tags or revisions +ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars +ENV ENVVARS_GIT_BRANCH=0.0.1 +ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore +ENV S3FILESTORE_GIT_BRANCH=master + +RUN apk add --no-cache \ + git \ + curl \ + python2 \ + python2-dev + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Fetch and build the default CKAN extensions +RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars +RUN pip wheel --wheel-dir=/wheels git+${S3FILESTORE_GIT_URL}@${S3FILESTORE_GIT_BRANCH}#egg=ckanext-s3filestore +RUN pip wheel --wheel-dir=/wheels -r https://raw.githubusercontent.com/keitaroinc/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt +RUN curl -o /wheels/s3filestore.txt https://raw.githubusercontent.com/keitaroinc/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt + +############ +### MAIN ### +############ +FROM alpine:3.12 + +MAINTAINER Keitaro Inc + +ENV APP_DIR=/srv/app +ENV SRC_DIR=/srv/app/src +ENV DATA_DIR=/srv/app/data +ENV PIP_SRC=${SRC_DIR} +ENV CKAN_SITE_URL=http://localhost:5000 +ENV CKAN__PLUGINS envvars s3filestore image_view text_view recline_view datastore datapusher + +WORKDIR ${APP_DIR} + +# Install necessary packages to run CKAN +RUN apk add --no-cache git \ + bash \ + gettext \ + curl \ + python2 \ + libmagic \ + pcre \ + libxslt \ + libxml2 \ + tzdata \ + apache2-utils && \ + # Install version 9.x of postgresql-client so that CKAN can run + apk add --repository http://dl-cdn.alpinelinux.org/alpine/v3.6/main \ + postgresql-client~=9.6 && \ + # Create SRC_DIR + mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Get artifacts from build stages +COPY --from=ckanbuild /wheels /srv/app/wheels +COPY --from=extbuild /wheels /srv/app/ext_wheels +COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan + +# Additional install steps for build stages artifacts +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent + +# Create a local user and group to run the app +RUN addgroup -g 92 -S ckan && \ + adduser -u 92 -h /srv/app -H -D -S -G ckan ckan + +# Install CKAN +RUN pip install -e /srv/app/src/ckan && \ + cd ${SRC_DIR}/ckan && \ + cp who.ini ${APP_DIR} && \ + pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ + # Install default CKAN extensions + pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars ckanext-s3filestore && \ + pip install --no-index --find-links=/srv/app/ext_wheels -r /srv/app/ext_wheels/s3filestore.txt && \ + # Create and update CKAN config + # Set timezone + echo "UTC" > /etc/timezone && \ + # Generate CKAN config + paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ + paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + # Create the data directory + mkdir ${DATA_DIR} && \ + # Change ownership to app user + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/wheels /srv/app/ext_wheels + +# Copy necessary scripts +COPY setup/app ${APP_DIR} + +# Create entrypoint directory for children image scripts +ONBUILD RUN mkdir docker-entrypoint.d + +EXPOSE 5000 + +HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 + +USER ckan + +CMD ["/srv/app/start_ckan.sh"] diff --git a/rootfs/setup/app/extra_scripts.sh b/images/ckan/2.7/setup/app/extra_scripts.sh similarity index 100% rename from rootfs/setup/app/extra_scripts.sh rename to images/ckan/2.7/setup/app/extra_scripts.sh diff --git a/rootfs/setup/app/prerun.py b/images/ckan/2.7/setup/app/prerun.py similarity index 95% rename from rootfs/setup/app/prerun.py rename to images/ckan/2.7/setup/app/prerun.py index 587e534..0dbd70e 100644 --- a/rootfs/setup/app/prerun.py +++ b/images/ckan/2.7/setup/app/prerun.py @@ -50,7 +50,6 @@ def check_solr_connection(retry=None): try: connection = urllib2.urlopen(search_url) except urllib2.URLError as e: - print((str(e))) print('[prerun] Unable to connect to solr...try again in a while.') import time time.sleep(10) @@ -58,7 +57,6 @@ def check_solr_connection(retry=None): else: import re conn_info = connection.read() - conn_info = re.sub(r'"zkConnected":true', '"zkConnected":True', conn_info) eval(conn_info) def init_db(): @@ -75,7 +73,6 @@ def init_db(): print('[prerun] Initializing or upgrading db - end') except subprocess.CalledProcessError as e: if 'OperationalError' in e.output: - print((e.output)) print('[prerun] Database not ready, waiting a bit before exit...') import time time.sleep(5) @@ -183,4 +180,3 @@ if __name__ == '__main__': if os.environ.get('CKAN_DATASTORE_WRITE_URL'): init_datastore() create_sysadmin() - #time.sleep(60000) # don't end the prerun script to allow container dock and debug diff --git a/rootfs/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh similarity index 100% rename from rootfs/setup/app/start_ckan.sh rename to images/ckan/2.7/setup/app/start_ckan.sh diff --git a/rootfs/setup/app/uwsgi.conf b/images/ckan/2.7/setup/app/uwsgi.conf similarity index 100% rename from rootfs/setup/app/uwsgi.conf rename to images/ckan/2.7/setup/app/uwsgi.conf diff --git a/rootfs/Dockerfile b/images/ckan/2.8/Dockerfile similarity index 95% rename from rootfs/Dockerfile rename to images/ckan/2.8/Dockerfile index 9c781a9..1c838b6 100644 --- a/rootfs/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -5,7 +5,7 @@ FROM alpine:3.12 as ckanbuild # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.4 +ENV GIT_BRANCH=ckan-2.8.5 # Set src dirs ENV SRC_DIR=/srv/app/src @@ -45,7 +45,7 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi gevent +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 ########################### @@ -64,7 +64,7 @@ ENV DEFAULT_EXTENSIONS envvars s3filestore ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars ENV ENVVARS_GIT_BRANCH=0.0.1 ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore -ENV S3FILESTORE_GIT_BRANCH=efd5711 +ENV S3FILESTORE_GIT_BRANCH=master RUN apk add --no-cache \ git \ @@ -94,6 +94,7 @@ MAINTAINER Keitaro Inc ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src +ENV DATA_DIR=/srv/app/data ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS envvars s3filestore image_view text_view recline_view datastore datapusher @@ -146,6 +147,8 @@ RUN pip install -e /srv/app/src/ckan && \ # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + # Create the data directory + mkdir ${DATA_DIR} && \ # Change ownership to app user chown -R ckan:ckan /srv/app diff --git a/images/ckan/2.8/setup/app/extra_scripts.sh b/images/ckan/2.8/setup/app/extra_scripts.sh new file mode 100755 index 0000000..80a70ef --- /dev/null +++ b/images/ckan/2.8/setup/app/extra_scripts.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# this is called before uwsgi is executed +# uset his to add extra scripts before ckan is started diff --git a/images/ckan/2.8/setup/app/prerun.py b/images/ckan/2.8/setup/app/prerun.py new file mode 100644 index 0000000..0dbd70e --- /dev/null +++ b/images/ckan/2.8/setup/app/prerun.py @@ -0,0 +1,182 @@ +import os +import sys +import subprocess +import psycopg2 +import urllib2 +import re + +import time + +ckan_ini = os.environ.get('CKAN_INI', '/srv/app/production.ini') + +RETRY = 5 + +def check_db_connection(retry=None): + + print('[prerun] Start check_db_connection...') + + if retry is None: + retry = RETRY + elif retry == 0: + print('[prerun] Giving up after 5 tries...') + sys.exit(1) + + conn_str = os.environ.get('CKAN_SQLALCHEMY_URL', '') + try: + connection = psycopg2.connect(conn_str) + + except psycopg2.Error as e: + print((str(e))) + print('[prerun] Unable to connect to the database...try again in a while.') + import time + time.sleep(10) + check_db_connection(retry = retry - 1) + else: + connection.close() + +def check_solr_connection(retry=None): + + print('[prerun] Start check_solr_connection...') + + if retry is None: + retry = RETRY + elif retry == 0: + print('[prerun] Giving up after 5 tries...') + sys.exit(1) + + url = os.environ.get('CKAN_SOLR_URL', '') + search_url = '{url}/select/?q=*&wt=json'.format(url=url) + + try: + connection = urllib2.urlopen(search_url) + except urllib2.URLError as e: + print('[prerun] Unable to connect to solr...try again in a while.') + import time + time.sleep(10) + check_solr_connection(retry = retry - 1) + else: + import re + conn_info = connection.read() + eval(conn_info) + +def init_db(): + + print('[prerun] Start init_db...') + + db_command = ['paster', '--plugin=ckan', 'db', 'init', '-c', ckan_ini] + + print('[prerun] Initializing or upgrading db - start using paster db init') + try: + # run init scripts + subprocess.check_output(db_command, stderr=subprocess.STDOUT) + + print('[prerun] Initializing or upgrading db - end') + except subprocess.CalledProcessError as e: + if 'OperationalError' in e.output: + print('[prerun] Database not ready, waiting a bit before exit...') + import time + time.sleep(5) + sys.exit(1) + else: + print((e.output)) + raise e + print('[prerun] Initializing or upgrading db - finish') + + +def init_datastore(): + + conn_str = os.environ.get('CKAN_DATASTORE_WRITE_URL') + if not conn_str: + print('[prerun] Skipping datastore initialization') + return + + datastore_perms_command = ['paster', '--plugin=ckan', 'datastore', + 'set-permissions', '-c', ckan_ini] + + connection = psycopg2.connect(conn_str) + cursor = connection.cursor() + + print('[prerun] Initializing datastore db - start') + try: + datastore_perms = subprocess.Popen( + datastore_perms_command, + stdout=subprocess.PIPE) + + perms_sql = datastore_perms.stdout.read() + # Remove internal pg command as psycopg2 does not like it + perms_sql = re.sub('\\\\connect \"(.*)\"', '', perms_sql.decode('utf-8')) + cursor.execute(perms_sql) + for notice in connection.notices: + print(notice) + + connection.commit() + + print('[prerun] Initializing datastore db - end') + print((datastore_perms.stdout.read())) + except psycopg2.Error as e: + print('[prerun] Could not initialize datastore') + print((str(e))) + + except subprocess.CalledProcessError as e: + if 'OperationalError' in e.output: + print((e.output)) + print('[prerun] Database not ready, waiting a bit before exit...') + time.sleep(5) + sys.exit(1) + else: + print((e.output)) + raise e + finally: + cursor.close() + connection.close() + + +def create_sysadmin(): + + print('[prerun] Start create_sysadmin...') + + name = os.environ.get('CKAN_SYSADMIN_NAME') + password = os.environ.get('CKAN_SYSADMIN_PASSWORD') + email = os.environ.get('CKAN_SYSADMIN_EMAIL') + + if name and password and email: + + # Check if user exists + command = ['paster', '--plugin=ckan', 'user', name, '-c', ckan_ini] + + out = subprocess.check_output(command) + if 'User:None' not in re.sub(r'\s', '', out.decode('utf-8')): + print('[prerun] Sysadmin user exists, skipping creation') + return + + # Create user + command = ['paster', '--plugin=ckan', 'user', 'add', + name, + 'password=' + password, + 'email=' + email, + '-c', ckan_ini] + + subprocess.call(command) + print(('[prerun] Created user {0}'.format(name))) + + # Make it sysadmin + command = ['paster', '--plugin=ckan', 'sysadmin', 'add', + name, + '-c', ckan_ini] + + subprocess.call(command) + print(('[prerun] Made user {0} a sysadmin'.format(name))) + +if __name__ == '__main__': + + maintenance = os.environ.get('MAINTENANCE_MODE', '').lower() == 'true' + + if maintenance: + print('[prerun] Maintenance mode, skipping setup...') + else: + check_db_connection() + check_solr_connection() + init_db() + if os.environ.get('CKAN_DATASTORE_WRITE_URL'): + init_datastore() + create_sysadmin() diff --git a/images/ckan/2.8/setup/app/start_ckan.sh b/images/ckan/2.8/setup/app/start_ckan.sh new file mode 100755 index 0000000..f870cb8 --- /dev/null +++ b/images/ckan/2.8/setup/app/start_ckan.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Run any startup scripts provided by images extending this one +if [[ -d "${APP_DIR}/docker-entrypoint.d" ]] +then + for f in ${APP_DIR}/docker-entrypoint.d/*; do + case "$f" in + *.sh) echo "$0: Running init file $f"; . "$f" ;; + *.py) echo "$0: Running init file $f"; python "$f"; echo ;; + *) echo "$0: Ignoring $f (not an sh or py file)" ;; + esac + echo + done +fi + +# Set the common uwsgi options +UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --paste-logger /srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L --gevent-early-monkey-patch" + +# Run the prerun script to init CKAN and create the default admin user +python prerun.py + +# Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully +if [ $? -eq 0 ] +then + if [ "$PASSWORD_PROTECT" = true ] + then + if [ "$HTPASSWD_USER" ] || [ "$HTPASSWD_PASSWORD" ] + then + # Generate htpasswd file for basicauth + htpasswd -d -b -c /srv/app/.htpasswd $HTPASSWD_USER $HTPASSWD_PASSWORD + # Start uwsgi with basicauth + uwsgi --ini /srv/app/uwsgi.conf --pcre-jit $UWSGI_OPTS + else + echo "Missing HTPASSWD_USER or HTPASSWD_PASSWORD environment variables. Exiting..." + exit 1 + fi + else + # Start uwsgi + uwsgi $UWSGI_OPTS + fi +else + echo "[prerun] failed...not starting CKAN." +fi diff --git a/images/ckan/2.8/setup/app/uwsgi.conf b/images/ckan/2.8/setup/app/uwsgi.conf new file mode 100644 index 0000000..6321d6d --- /dev/null +++ b/images/ckan/2.8/setup/app/uwsgi.conf @@ -0,0 +1,2 @@ +[uwsgi] +route = ^(?!/api).*$ basicauth:Restricted,/srv/app/.htpasswd diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile new file mode 100644 index 0000000..c65125b --- /dev/null +++ b/images/ckan/2.9/Dockerfile @@ -0,0 +1,179 @@ +################## +### Build CKAN ### +################## +FROM alpine:3.12 as ckanbuild + +# Set CKAN version to build +ENV GIT_URL=https://github.com/ckan/ckan.git +ENV GIT_BRANCH=ckan-2.9.0 + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +WORKDIR ${SRC_DIR} + +# Packages to build CKAN requirements and plugins +RUN apk add --no-cache \ + git \ + curl \ + python3 \ + postgresql-dev \ + linux-headers \ + gcc \ + make \ + g++ \ + autoconf \ + automake \ + libtool \ + musl-dev \ + pcre-dev \ + pcre \ + python3-dev \ + libffi-dev \ + libxml2-dev \ + libxslt-dev + +# Link python to python3 +RUN ln -s /usr/bin/python3 /usr/bin/python + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Downgrade setuptools so that CKAN requirements can be built +RUN pip install setuptools==44.1.0 + +# Fetch and build CKAN and requirements +RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan +RUN rm -rf /srv/app/src/ckan/.git +RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 + + +########################### +### Default-Extensions #### +########################### +FROM alpine:3.12 as extbuild + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +# List of default extensions +ENV DEFAULT_EXTENSIONS envvars + +# Locations and tags, please use specific tags or revisions +ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars +ENV ENVVARS_GIT_BRANCH=0.0.1 + +RUN apk add --no-cache \ + git \ + curl \ + python3 \ + python3-dev + +# Link python to python3 +RUN ln -s /usr/bin/python3 /usr/bin/python + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Fetch and build the default CKAN extensions +RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars + +############ +### MAIN ### +############ +FROM alpine:3.12 + +MAINTAINER Keitaro Inc + +ENV APP_DIR=/srv/app +ENV SRC_DIR=/srv/app/src +ENV DATA_DIR=/srv/app/data +ENV PIP_SRC=${SRC_DIR} +ENV CKAN_SITE_URL=http://localhost:5000 +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher + +WORKDIR ${APP_DIR} + +# Install necessary packages to run CKAN +RUN apk add --no-cache git \ + bash \ + gettext \ + curl \ + postgresql-client \ + python3 \ + libmagic \ + pcre \ + libxslt \ + libxml2 \ + tzdata \ + apache2-utils && \ + # Create SRC_DIR + mkdir -p ${SRC_DIR} && \ + # Link python to python3 + ln -s /usr/bin/python3 /usr/bin/python + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Get artifacts from build stages +COPY --from=ckanbuild /wheels /srv/app/wheels +COPY --from=extbuild /wheels /srv/app/ext_wheels +COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan + +# Additional install steps for build stages artifacts +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent + +# Create a local user and group to run the app +RUN addgroup -g 92 -S ckan && \ + adduser -u 92 -h /srv/app -H -D -S -G ckan ckan + +# Install CKAN +RUN pip install -e /srv/app/src/ckan && \ + cd ${SRC_DIR}/ckan && \ + cp who.ini ${APP_DIR} && \ + pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ + # Install default CKAN extensions + pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars && \ + # Create and update CKAN config + # Set timezone + echo "UTC" > /etc/timezone && \ + # Generate CKAN config + ckan generate config ${APP_DIR}/production.ini && \ + # Not working atm since ckan config tool tries to load config before executing config-tool, workaround + # ckan -c ${APP_DIR}/production.ini config-tool "ckan.plugins = ${CKAN__PLUGINS}" && \ + sed -i "/ckan.plugins = stats/c ckan.plugins = ${CKAN__PLUGINS}" ${APP_DIR}/production.ini && \ + # Create the data directory + mkdir ${DATA_DIR} && \ + # Webassets can't be loaded from env variables at runtime, it needs to be in the config so that it is created + sed -i "/ckan.webassets.path = /c ckan.webassets.path = ${DATA_DIR}/webassets" ${APP_DIR}/production.ini && \ + # Change ownership to app user + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/wheels /srv/app/ext_wheels + +# Copy necessary scripts +COPY setup/app ${APP_DIR} + +# Create entrypoint directory for children image scripts +ONBUILD RUN mkdir docker-entrypoint.d + +EXPOSE 5000 + +HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 + +USER ckan + +CMD ["/srv/app/start_ckan.sh"] diff --git a/images/ckan/2.9/setup/app/extra_scripts.sh b/images/ckan/2.9/setup/app/extra_scripts.sh new file mode 100755 index 0000000..80a70ef --- /dev/null +++ b/images/ckan/2.9/setup/app/extra_scripts.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# this is called before uwsgi is executed +# uset his to add extra scripts before ckan is started diff --git a/images/ckan/2.9/setup/app/prerun.py b/images/ckan/2.9/setup/app/prerun.py new file mode 100644 index 0000000..01e766e --- /dev/null +++ b/images/ckan/2.9/setup/app/prerun.py @@ -0,0 +1,182 @@ +import os +import sys +import subprocess +import psycopg2 +import urllib.request, urllib.error, urllib.parse +import re + +import time + +ckan_ini = os.environ.get('CKAN_INI', '/srv/app/production.ini') + +RETRY = 5 + +def check_db_connection(retry=None): + + print('[prerun] Start check_db_connection...') + + if retry is None: + retry = RETRY + elif retry == 0: + print('[prerun] Giving up after 5 tries...') + sys.exit(1) + + conn_str = os.environ.get('CKAN_SQLALCHEMY_URL', '') + try: + connection = psycopg2.connect(conn_str) + + except psycopg2.Error as e: + print((str(e))) + print('[prerun] Unable to connect to the database...try again in a while.') + import time + time.sleep(10) + check_db_connection(retry = retry - 1) + else: + connection.close() + +def check_solr_connection(retry=None): + + print('[prerun] Start check_solr_connection...') + + if retry is None: + retry = RETRY + elif retry == 0: + print('[prerun] Giving up after 5 tries...') + sys.exit(1) + + url = os.environ.get('CKAN_SOLR_URL', '') + search_url = '{url}/select/?q=*&wt=json'.format(url=url) + + try: + connection = urllib.request.urlopen(search_url) + except urllib.error.URLError as e: + print('[prerun] Unable to connect to solr...try again in a while.') + import time + time.sleep(10) + check_solr_connection(retry = retry - 1) + else: + import re + conn_info = connection.read() + print(conn_info) + eval(conn_info) + +def init_db(): + + print('[prerun] Start init_db...') + + db_command = ['ckan', '-c', ckan_ini, 'db', 'init'] + + print('[prerun] Initializing or upgrading db - start using ckan db init') + try: + # run init scripts + subprocess.check_output(db_command, stderr=subprocess.STDOUT) + + print('[prerun] Initializing or upgrading db - end') + except subprocess.CalledProcessError as e: + if 'OperationalError' in str(e.output): + print(e.output.decode('utf-8')) + print('[prerun] Database not ready, waiting a bit before exit...') + import time + time.sleep(5) + sys.exit(1) + else: + print(e.output.decode('utf-8')) + raise e + print('[prerun] Initializing or upgrading db - finish') + + +def init_datastore(): + + conn_str = os.environ.get('CKAN_DATASTORE_WRITE_URL') + if not conn_str: + print('[prerun] Skipping datastore initialization') + return + + datastore_perms_command = ['ckan', '-c', ckan_ini, 'datastore', + 'set-permissions'] + + connection = psycopg2.connect(conn_str) + cursor = connection.cursor() + + print('[prerun] Initializing datastore db - start') + try: + datastore_perms = subprocess.Popen( + datastore_perms_command, + stdout=subprocess.PIPE) + + perms_sql = datastore_perms.stdout.read() + # Remove internal pg command as psycopg2 does not like it + perms_sql = re.sub('\\\\connect \"(.*)\"', '', perms_sql.decode('utf-8')) + cursor.execute(perms_sql) + for notice in connection.notices: + print(notice) + + connection.commit() + + print('[prerun] Initializing datastore db - end') + print((datastore_perms.stdout.read())) + except psycopg2.Error as e: + print('[prerun] Could not initialize datastore') + print(e.decode('utf-8')) + + except subprocess.CalledProcessError as e: + if 'OperationalError' in str(e.output): + print(e.output.decode('utf-8')) + print('[prerun] Database not ready, waiting a bit before exit...') + time.sleep(5) + sys.exit(1) + else: + print(e.output.decode('utf-8')) + raise e + finally: + cursor.close() + connection.close() + + +def create_sysadmin(): + + print('[prerun] Start create_sysadmin...') + + name = os.environ.get('CKAN_SYSADMIN_NAME') + password = os.environ.get('CKAN_SYSADMIN_PASSWORD') + email = os.environ.get('CKAN_SYSADMIN_EMAIL') + + if name and password and email: + + # Check if user exists + command = ['ckan', '-c', ckan_ini, 'user', 'show', name] + + out = subprocess.check_output(command) + if 'User:None' not in re.sub(r'\s', '', out.decode('utf-8')): + print('[prerun] Sysadmin user exists, skipping creation') + return + + # Create user + command = ['ckan', '-c', ckan_ini, 'user', 'add', + name, + 'password=' + password, + 'email=' + email] + + subprocess.call(command) + print(('[prerun] Created user {0}'.format(name))) + + # Make it sysadmin + command = ['ckan', '-c', ckan_ini, 'sysadmin', 'add', + name] + + subprocess.call(command) + print(('[prerun] Made user {0} a sysadmin'.format(name))) + +if __name__ == '__main__': + + maintenance = os.environ.get('MAINTENANCE_MODE', '').lower() == 'true' + + if maintenance: + print('[prerun] Maintenance mode, skipping setup...') + else: + check_db_connection() + check_solr_connection() + init_db() + if os.environ.get('CKAN_DATASTORE_WRITE_URL'): + init_datastore() + create_sysadmin() diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh new file mode 100755 index 0000000..e8f67ae --- /dev/null +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Run any startup scripts provided by images extending this one +if [[ -d "${APP_DIR}/docker-entrypoint.d" ]] +then + for f in ${APP_DIR}/docker-entrypoint.d/*; do + case "$f" in + *.sh) echo "$0: Running init file $f"; . "$f" ;; + *.py) echo "$0: Running init file $f"; python "$f"; echo ;; + *) echo "$0: Ignoring $f (not an sh or py file)" ;; + esac + echo + done +fi + +# Set the common uwsgi options +UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid ckan --gid ckan --http :5000 --master --enable-threads --wsgi-file /srv/app/wsgi.py --module wsgi:application --lazy-apps --gevent 2000 -p 2 -L --gevent-early-monkey-patch --vacuum --harakiri 50 --callable application" + +# Run the prerun script to init CKAN and create the default admin user +python prerun.py + +# Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully +if [ $? -eq 0 ] +then + if [ "$PASSWORD_PROTECT" = true ] + then + if [ "$HTPASSWD_USER" ] || [ "$HTPASSWD_PASSWORD" ] + then + # Generate htpasswd file for basicauth + htpasswd -d -b -c /srv/app/.htpasswd $HTPASSWD_USER $HTPASSWD_PASSWORD + # Start uwsgi with basicauth + uwsgi --ini /srv/app/uwsgi.conf --pcre-jit $UWSGI_OPTS + else + echo "Missing HTPASSWD_USER or HTPASSWD_PASSWORD environment variables. Exiting..." + exit 1 + fi + else + # Start uwsgi + uwsgi $UWSGI_OPTS + fi +else + echo "[prerun] failed...not starting CKAN." +fi diff --git a/images/ckan/2.9/setup/app/uwsgi.conf b/images/ckan/2.9/setup/app/uwsgi.conf new file mode 100644 index 0000000..6321d6d --- /dev/null +++ b/images/ckan/2.9/setup/app/uwsgi.conf @@ -0,0 +1,2 @@ +[uwsgi] +route = ^(?!/api).*$ basicauth:Restricted,/srv/app/.htpasswd diff --git a/images/ckan/2.9/setup/app/wsgi.py b/images/ckan/2.9/setup/app/wsgi.py new file mode 100644 index 0000000..2ad03a7 --- /dev/null +++ b/images/ckan/2.9/setup/app/wsgi.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +import os +from ckan.config.middleware import make_app +from ckan.cli import CKANConfigLoader +from logging.config import fileConfig as loggingFileConfig +config_filepath = os.path.join( + os.path.dirname(os.path.abspath(__file__)), u'production.ini') +abspath = os.path.join(os.path.dirname(os.path.abspath(__file__))) +loggingFileConfig(config_filepath) +config = CKANConfigLoader(config_filepath).get_config() +application = make_app(config) diff --git a/images/datapusher/Dockerfile b/images/datapusher/Dockerfile new file mode 100644 index 0000000..5bff8c4 --- /dev/null +++ b/images/datapusher/Dockerfile @@ -0,0 +1,100 @@ +############# +### Build ### +############# +FROM alpine:3.12 as build + +# Set datapusher version to build +ENV GIT_URL https://github.com/keitaroinc/datapusher.git +ENV GIT_BRANCH parametrize-job-config +ENV REQUIREMENTS_URL https://raw.githubusercontent.com/keitaroinc/datapusher/${GIT_BRANCH}/requirements.txt + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +WORKDIR ${SRC_DIR} + +# Packages to build datapusher +RUN apk add --no-cache \ + python3 \ + curl \ + gcc \ + make \ + g++ \ + autoconf \ + automake \ + libtool \ + git \ + musl-dev \ + python3-dev \ + libffi-dev \ + openssl-dev \ + libxml2-dev \ + libxslt-dev + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python3 ${SRC_DIR}/get-pip.py + +# Fetch and build datapusher and requirements +RUN pip wheel --wheel-dir=/wheels git+${GIT_URL}@${GIT_BRANCH}#egg=datapusher +RUN pip wheel --wheel-dir=/wheels -r ${REQUIREMENTS_URL} +RUN curl -o /wheels/requirements.txt ${REQUIREMENTS_URL} + +# Get uwsgi and gevent from pip +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 + + +############ +### MAIN ### +############ +FROM alpine:3.12 + +MAINTAINER Keitaro Inc + +ENV APP_DIR=/srv/app +ENV JOB_CONFIG ${APP_DIR}/datapusher_settings.py + +WORKDIR ${APP_DIR} + +RUN apk add --no-cache \ + python3 \ + curl \ + libmagic \ + libxslt + +# Install pip +RUN curl -o ${src_dir}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python3 ${src_dir}/get-pip.py + +# Get artifacts from build stages +COPY --from=build /wheels /srv/app/wheels + +# Install uwsgi and gevent +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent + +# Create a local user and group to run the app +RUN addgroup -g 92 -S ckan && \ + adduser -u 92 -h /srv/app -H -D -S -G ckan ckan + +# Install datapusher +RUN pip install --no-index --find-links=/srv/app/wheels datapusher && \ + pip install --no-index --find-links=/srv/app/wheels -r /srv/app/wheels/requirements.txt && \ + # Set timezone + echo "UTC" > /etc/timezone && \ + # Change ownership to app user + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/wheels + +COPY setup ${APP_DIR} + +EXPOSE 8000 + +USER ckan + +CMD ["uwsgi", "--socket=/tmp/uwsgi.sock", "--uid=92", "--gid=92", "--http=:8000", "--master", "--enable-threads", "--gevent=2000", "-p 2", "-L", "--wsgi-file=wsgi.py"] diff --git a/images/datapusher/setup/datapusher_settings.py b/images/datapusher/setup/datapusher_settings.py new file mode 100644 index 0000000..cf0a138 --- /dev/null +++ b/images/datapusher/setup/datapusher_settings.py @@ -0,0 +1,33 @@ +import uuid +import os + +DEBUG = False +TESTING = False +SECRET_KEY = str(uuid.uuid4()) +USERNAME = str(uuid.uuid4()) +PASSWORD = str(uuid.uuid4()) + +NAME = 'datapusher' + +# database + +SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/job_store.db' + +# webserver host and port + +HOST = '0.0.0.0' +PORT = 8000 + +# logging + +#FROM_EMAIL = 'server-error@example.com' +#ADMINS = ['yourname@example.com'] # where to send emails + +#LOG_FILE = '/tmp/ckan_service.log' +STDERR = True + +# Content length settings +MAX_CONTENT_LENGTH = int(os.environ.get('DATAPUSHER_MAX_CONTENT_LENGTH', '1024000')) +CHUNK_SIZE = int(os.environ.get('DATAPUSHER_CHUNK_SIZE', '16384')) +CHUNK_INSERT_ROWS = int(os.environ.get('DATAPUSHER_CHUNK_INSERT_ROWS', '250')) +DOWNLOAD_TIMEOUT = int(os.environ.get('DATAPUSHER_DOWNLOAD_TIMEOUT', '30')) diff --git a/images/datapusher/setup/wsgi.py b/images/datapusher/setup/wsgi.py new file mode 100644 index 0000000..8701ffa --- /dev/null +++ b/images/datapusher/setup/wsgi.py @@ -0,0 +1,9 @@ +import os +import sys + +import ckanserviceprovider.web as web +web.init() + +import datapusher.jobs as jobs + +application = web.app From 348dfb841e36ccc3138c46781d561c04966360da Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 11 Sep 2020 12:12:41 +0200 Subject: [PATCH 048/213] Remove s3 as a default plugin from base images --- images/ckan/2.7/Dockerfile | 12 +++--------- images/ckan/2.8/Dockerfile | 12 +++--------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 1a4c72e..74878a7 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -62,13 +62,11 @@ ENV SRC_DIR=/srv/app/src ENV PIP_SRC=${SRC_DIR} # List of default extensions -ENV DEFAULT_EXTENSIONS envvars s3filestore +ENV DEFAULT_EXTENSIONS envvars # Locations and tags, please use specific tags or revisions ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars ENV ENVVARS_GIT_BRANCH=0.0.1 -ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore -ENV S3FILESTORE_GIT_BRANCH=master RUN apk add --no-cache \ git \ @@ -85,9 +83,6 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars -RUN pip wheel --wheel-dir=/wheels git+${S3FILESTORE_GIT_URL}@${S3FILESTORE_GIT_BRANCH}#egg=ckanext-s3filestore -RUN pip wheel --wheel-dir=/wheels -r https://raw.githubusercontent.com/keitaroinc/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt -RUN curl -o /wheels/s3filestore.txt https://raw.githubusercontent.com/keitaroinc/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt ############ ### MAIN ### @@ -101,7 +96,7 @@ ENV SRC_DIR=/srv/app/src ENV DATA_DIR=/srv/app/data ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 -ENV CKAN__PLUGINS envvars s3filestore image_view text_view recline_view datastore datapusher +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher WORKDIR ${APP_DIR} @@ -145,8 +140,7 @@ RUN pip install -e /srv/app/src/ckan && \ cp who.ini ${APP_DIR} && \ pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ # Install default CKAN extensions - pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars ckanext-s3filestore && \ - pip install --no-index --find-links=/srv/app/ext_wheels -r /srv/app/ext_wheels/s3filestore.txt && \ + pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars && \ # Create and update CKAN config # Set timezone echo "UTC" > /etc/timezone && \ diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 1c838b6..ad29d5d 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -58,13 +58,11 @@ ENV SRC_DIR=/srv/app/src ENV PIP_SRC=${SRC_DIR} # List of default extensions -ENV DEFAULT_EXTENSIONS envvars s3filestore +ENV DEFAULT_EXTENSIONS envvars # Locations and tags, please use specific tags or revisions ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars ENV ENVVARS_GIT_BRANCH=0.0.1 -ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore -ENV S3FILESTORE_GIT_BRANCH=master RUN apk add --no-cache \ git \ @@ -81,9 +79,6 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars -RUN pip wheel --wheel-dir=/wheels git+${S3FILESTORE_GIT_URL}@${S3FILESTORE_GIT_BRANCH}#egg=ckanext-s3filestore -RUN pip wheel --wheel-dir=/wheels -r https://raw.githubusercontent.com/keitaroinc/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt -RUN curl -o /wheels/s3filestore.txt https://raw.githubusercontent.com/keitaroinc/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt ############ ### MAIN ### @@ -97,7 +92,7 @@ ENV SRC_DIR=/srv/app/src ENV DATA_DIR=/srv/app/data ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 -ENV CKAN__PLUGINS envvars s3filestore image_view text_view recline_view datastore datapusher +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher WORKDIR ${APP_DIR} @@ -139,8 +134,7 @@ RUN pip install -e /srv/app/src/ckan && \ cp who.ini ${APP_DIR} && \ pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ # Install default CKAN extensions - pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars ckanext-s3filestore && \ - pip install --no-index --find-links=/srv/app/ext_wheels -r /srv/app/ext_wheels/s3filestore.txt && \ + pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars && \ # Create and update CKAN config # Set timezone echo "UTC" > /etc/timezone && \ From 3f0ebece311b824fc7b0bba4e0548c02d722f278 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 11 Sep 2020 12:14:39 +0200 Subject: [PATCH 049/213] Add s3filestore example --- compose/.ckan-env | 8 --- examples/Dockerfile | 52 ------------------- examples/s3filestore/.ckan-env | 33 ++++++++++++ examples/s3filestore/.env | 29 +++++++++++ examples/s3filestore/Dockerfile | 43 +++++++++++++++ .../s3filestore/docker-compose.yml | 36 +++++++++---- 6 files changed, 131 insertions(+), 70 deletions(-) delete mode 100644 examples/Dockerfile create mode 100644 examples/s3filestore/.ckan-env create mode 100644 examples/s3filestore/.env create mode 100644 examples/s3filestore/Dockerfile rename compose/docker-compose-minio.yml => examples/s3filestore/docker-compose.yml (86%) diff --git a/compose/.ckan-env b/compose/.ckan-env index cb42e24..344ee7d 100644 --- a/compose/.ckan-env +++ b/compose/.ckan-env @@ -23,11 +23,3 @@ CKAN_SMTP_STARTTLS=True CKAN_SMTP_USER=user CKAN_SMTP_PASSWORD=pass CKAN_SMTP_MAIL_FROM=ckan@localhost - -# S3/MINIO settings -#CKANEXT__S3FILESTORE__AWS_ACCESS_KEY_ID = MINIOACCESSKEY -#CKANEXT__S3FILESTORE__AWS_SECRET_ACCESS_KEY = MINIOSECRETKEY -#CKANEXT__S3FILESTORE__AWS_BUCKET_NAME = ckan -#CKANEXT__S3FILESTORE__HOST_NAME = http://minio:9000 -#CKANEXT__S3FILESTORE__REGION_NAME = us-east-1 -#CKANEXT__S3FILESTORE__SIGNATURE_VERSION = s3v4 diff --git a/examples/Dockerfile b/examples/Dockerfile deleted file mode 100644 index eee81a3..0000000 --- a/examples/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -################### -### Extensions #### -################### -FROM keitaro/ckan:2.9.0 as extbuild - -MAINTAINER Keitaro Inc - -# Locations and tags, please use specific tags or revisions -ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest -ENV HARVEST_GIT_BRANCH=v1.3.1 - -# Switch to the root user -USER root - -# Install necessary packages to build extensions -RUN apk add --no-cache \ - gcc \ - g++ \ - libffi-dev \ - openssl-dev \ - python3-dev - -# Fetch and build the custom CKAN extensions -RUN pip wheel --wheel-dir=/wheels git+${HARVEST_GIT_URL}@${HARVEST_GIT_BRANCH}#egg=ckanext-harvest -RUN pip wheel --wheel-dir=/wheels -r https://raw.githubusercontent.com/ckan/ckanext-harvest/${HARVEST_GIT_BRANCH}/pip-requirements.txt -RUN curl -o /wheels/harvest.txt https://raw.githubusercontent.com/ckan/ckanext-harvest/${HARVEST_GIT_BRANCH}/pip-requirements.txt - -############ -### MAIN ### -############ -FROM keitaro/ckan:2.9.0 - -ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher harvest ckan_harvester - -# Switch to the root user -USER root - -COPY --from=extbuild /wheels /srv/app/ext_wheels - -# Install and enable the custom extensions -RUN pip install --no-index --find-links=/srv/app/ext_wheels ckanext-harvest && \ - pip install --no-index --find-links=/srv/app/ext_wheels -r /srv/app/ext_wheels/harvest.txt && \ - # Not working atm since ckan config tool tries to load config before executing config-tool, workaround - # ckan -c ${APP_DIR}/production.ini config-tool "ckan.plugins = ${CKAN__PLUGINS}" && \ - sed -i "/ckan.plugins = envvars/c ckan.plugins = ${CKAN__PLUGINS}" ${APP_DIR}/production.ini && \ - chown -R ckan:ckan /srv/app - -# Remove wheels -RUN rm -rf /srv/app/ext_wheels - -# Switch to the ckan user -USER ckan diff --git a/examples/s3filestore/.ckan-env b/examples/s3filestore/.ckan-env new file mode 100644 index 0000000..3b9b9ef --- /dev/null +++ b/examples/s3filestore/.ckan-env @@ -0,0 +1,33 @@ +# Runtime configuration of CKAN enabled through ckanext-envvars +# Information about how it works: https://github.com/okfn/ckanext-envvars +# Note that variables here take presedence over build/up time variables in .env + +# General Settings +CKAN_SITE_ID=default +CKAN_SITE_URL=http://localhost:5000 +CKAN_PORT=5000 +CKAN_MAX_UPLOAD_SIZE_MB=10 +# CKAN Plugins +CKAN__PLUGINS=envvars s3filestore image_view text_view recline_view datastore datapusher +# CKAN requires storage path to be set in order for filestore to be enabled +CKAN__STORAGE_PATH=/srv/app/data +CKAN__WEBASSETS__PATH=/srv/app/data/webassets +# SYSADMIN settings, a sysadmin user is created automatically with the below credentials +CKAN_SYSADMIN_NAME=sysadmin +CKAN_SYSADMIN_PASSWORD=password +CKAN_SYSADMIN_EMAIL=sysadmin@ckantest.com + +# Email settings +CKAN_SMTP_SERVER=smtp.corporateict.domain:25 +CKAN_SMTP_STARTTLS=True +CKAN_SMTP_USER=user +CKAN_SMTP_PASSWORD=pass +CKAN_SMTP_MAIL_FROM=ckan@localhost + +# S3/MINIO settings +CKANEXT__S3FILESTORE__AWS_ACCESS_KEY_ID = MINIOACCESSKEY +CKANEXT__S3FILESTORE__AWS_SECRET_ACCESS_KEY = MINIOSECRETKEY +CKANEXT__S3FILESTORE__AWS_BUCKET_NAME = ckan +CKANEXT__S3FILESTORE__HOST_NAME = http://minio:9000 +CKANEXT__S3FILESTORE__REGION_NAME = us-east-1 +CKANEXT__S3FILESTORE__SIGNATURE_VERSION = s3v4 diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env new file mode 100644 index 0000000..4068526 --- /dev/null +++ b/examples/s3filestore/.env @@ -0,0 +1,29 @@ +# Variables in this file will be used as build arguments when running +# docker-compose build and docker-compose up +# Verify correct substitution with "docker-compose config" +# If variables are newly added or enabled, please delete and rebuild the images to pull in changes: +# docker-compose down -v +# docker-compose build +# docker-compose up -d + +# Database +POSTGRES_PASSWORD=ckan +POSTGRES_PORT=5432 +DATASTORE_READONLY_PASSWORD=datastore + +# CKAN +CKAN_VERSION=2.8.5 +CKAN_SITE_ID=default +CKAN_SITE_URL=http://localhost:5000 +CKAN_PORT=5000 +CKAN_MAX_UPLOAD_SIZE_MB=10 + +# Datapusher +DATAPUSHER_VERSION=0.0.17 +DATAPUSHER_MAX_CONTENT_LENGTH=10485760 +DATAPUSHER_CHUNK_SIZE=16384 +DATAPUSHER_CHUNK_INSERT_ROWS=250 +DATAPUSHER_DOWNLOAD_TIMEOUT=30 + +# Redis +REDIS_VERSION=6.0.7 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile new file mode 100644 index 0000000..1d566cf --- /dev/null +++ b/examples/s3filestore/Dockerfile @@ -0,0 +1,43 @@ +################### +### Extensions #### +################### +FROM keitaro/ckan:2.8.5 as extbuild + +MAINTAINER Keitaro Inc + +# Locations and tags, please use specific tags or revisions +ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore +ENV S3FILESTORE_GIT_BRANCH=master + +# Switch to the root user +USER root + +# Fetch and build the custom CKAN extensions +RUN pip wheel --wheel-dir=/wheels git+${S3FILESTORE_GIT_URL}@${S3FILESTORE_GIT_BRANCH}#egg=ckanext-s3filestore +RUN pip wheel --wheel-dir=/wheels -r https://raw.githubusercontent.com/keitaroinc/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt +RUN curl -o /wheels/s3filestore.txt https://raw.githubusercontent.com/keitaroinc/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt + +############ +### MAIN ### +############ +FROM keitaro/ckan:2.8.5 + +MAINTAINER Keitaro Inc + +ENV CKAN__PLUGINS envvars s3filestore image_view text_view recline_view datastore datapusher + +# Switch to the root user +USER root + +COPY --from=extbuild /wheels /srv/app/ext_wheels + +# Install and enable the custom extensions +RUN pip install --no-index --find-links=/srv/app/ext_wheels ckanext-s3filestore && \ + pip install --no-index --find-links=/srv/app/ext_wheels -r /srv/app/ext_wheels/s3filestore.txt && \ + paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/ext_wheels + +USER ckan diff --git a/compose/docker-compose-minio.yml b/examples/s3filestore/docker-compose.yml similarity index 86% rename from compose/docker-compose-minio.yml rename to examples/s3filestore/docker-compose.yml index e12a0ab..db64929 100644 --- a/compose/docker-compose-minio.yml +++ b/examples/s3filestore/docker-compose.yml @@ -9,12 +9,11 @@ volumes: services: ckan: container_name: ckan - image: keitaro/ckan:${CKAN_VERSION} - links: - - db - - solr - - redis - - minio + build: + context: . + networks: + - frontend + - backend depends_on: - db ports: @@ -36,8 +35,9 @@ services: datapusher: container_name: datapusher image: keitaro/ckan-datapusher:${DATAPUSHER_VERSION} - links: - - ckan + networks: + - frontend + - backend ports: - "8000:8000" environment: @@ -49,11 +49,13 @@ services: db: container_name: db build: - context: . + context: ../../compose dockerfile: postgresql/Dockerfile args: - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + networks: + - backend environment: - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} @@ -65,20 +67,27 @@ services: solr: container_name: solr build: - context: . + context: ../../compose dockerfile: solr/Dockerfile args: - CKAN_VERSION=${CKAN_VERSION} + networks: + - backend volumes: - solr_data:/opt/solr/server/solr/ckan/data redis: container_name: redis image: redis:${REDIS_VERSION} + networks: + - backend minio: container_name: minio image: minio/minio:RELEASE.2020-08-08T04-50-06Z + networks: + - backend + - frontend ports: - "0.0.0.0:9000:9000" environment: @@ -91,6 +100,8 @@ services: mc: container_name: mc image: minio/mc:RELEASE.2020-08-08T02-33-58Z + networks: + - backend depends_on: - minio entrypoint: > @@ -98,4 +109,9 @@ services: /usr/bin/mc config host rm local; /usr/bin/mc config host add --quiet --api s3v4 local http://minio:9000 minio ckan123; /usr/bin/mc mb --quiet local/ckan/; + /usr/bin/mc policy set public local/ckan; " + +networks: + frontend: + backend: From 9d42fbff449af7b02cffd2390c594ff774a59016 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 11 Sep 2020 12:53:25 +0200 Subject: [PATCH 050/213] Apply correct policy on minio bucket --- examples/s3filestore/docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/s3filestore/docker-compose.yml b/examples/s3filestore/docker-compose.yml index db64929..d195fbb 100644 --- a/examples/s3filestore/docker-compose.yml +++ b/examples/s3filestore/docker-compose.yml @@ -107,9 +107,9 @@ services: entrypoint: > /bin/sh -c " /usr/bin/mc config host rm local; - /usr/bin/mc config host add --quiet --api s3v4 local http://minio:9000 minio ckan123; - /usr/bin/mc mb --quiet local/ckan/; - /usr/bin/mc policy set public local/ckan; + /usr/bin/mc config host add --api s3v4 local http://minio:9000 MINIOACCESSKEY MINIOSECRETKEY; + /usr/bin/mc mb local/ckan/; + /usr/bin/mc policy set download local/ckan/storage; " networks: From a9d0fff9cb1db95591cd1f1ab57376fc2d9964a7 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 11 Sep 2020 13:33:41 +0200 Subject: [PATCH 051/213] Add harvest example --- examples/harvest/.ckan-env | 29 ++++ examples/harvest/.env | 29 ++++ examples/harvest/Dockerfile | 55 +++++++ examples/harvest/docker-compose.yml | 191 +++++++++++++++++++++++++ examples/harvest/scripts/00_harvest.sh | 2 + 5 files changed, 306 insertions(+) create mode 100644 examples/harvest/.ckan-env create mode 100644 examples/harvest/.env create mode 100644 examples/harvest/Dockerfile create mode 100644 examples/harvest/docker-compose.yml create mode 100644 examples/harvest/scripts/00_harvest.sh diff --git a/examples/harvest/.ckan-env b/examples/harvest/.ckan-env new file mode 100644 index 0000000..c1a0993 --- /dev/null +++ b/examples/harvest/.ckan-env @@ -0,0 +1,29 @@ +# Runtime configuration of CKAN enabled through ckanext-envvars +# Information about how it works: https://github.com/okfn/ckanext-envvars +# Note that variables here take presedence over build/up time variables in .env + +# General Settings +CKAN_SITE_ID=default +CKAN_SITE_URL=http://localhost:5000 +CKAN_PORT=5000 +CKAN_MAX_UPLOAD_SIZE_MB=10 +# CKAN Plugins +CKAN__PLUGINS=envvars image_view text_view recline_view datastore datapusher harvest ckan_harvester +# CKAN requires storage path to be set in order for filestore to be enabled +CKAN__STORAGE_PATH=/srv/app/data +CKAN__WEBASSETS__PATH=/srv/app/data/webassets +# SYSADMIN settings, a sysadmin user is created automatically with the below credentials +CKAN_SYSADMIN_NAME=sysadmin +CKAN_SYSADMIN_PASSWORD=password +CKAN_SYSADMIN_EMAIL=sysadmin@ckantest.com + +# Email settings +CKAN_SMTP_SERVER=smtp.corporateict.domain:25 +CKAN_SMTP_STARTTLS=True +CKAN_SMTP_USER=user +CKAN_SMTP_PASSWORD=pass +CKAN_SMTP_MAIL_FROM=ckan@localhost + +# Harvest settings +CKAN__HARVEST__MQ__TYPE=redis +CKAN__HARVEST__MQ__HOSTNAME=redis diff --git a/examples/harvest/.env b/examples/harvest/.env new file mode 100644 index 0000000..89b30ec --- /dev/null +++ b/examples/harvest/.env @@ -0,0 +1,29 @@ +# Variables in this file will be used as build arguments when running +# docker-compose build and docker-compose up +# Verify correct substitution with "docker-compose config" +# If variables are newly added or enabled, please delete and rebuild the images to pull in changes: +# docker-compose down -v +# docker-compose build +# docker-compose up -d + +# Database +POSTGRES_PASSWORD=ckan +POSTGRES_PORT=5432 +DATASTORE_READONLY_PASSWORD=datastore + +# CKAN +CKAN_VERSION=2.9.0 +CKAN_SITE_ID=default +CKAN_SITE_URL=http://localhost:5000 +CKAN_PORT=5000 +CKAN_MAX_UPLOAD_SIZE_MB=10 + +# Datapusher +DATAPUSHER_VERSION=0.0.17 +DATAPUSHER_MAX_CONTENT_LENGTH=10485760 +DATAPUSHER_CHUNK_SIZE=16384 +DATAPUSHER_CHUNK_INSERT_ROWS=250 +DATAPUSHER_DOWNLOAD_TIMEOUT=30 + +# Redis +REDIS_VERSION=6.0.7 diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile new file mode 100644 index 0000000..f12a33d --- /dev/null +++ b/examples/harvest/Dockerfile @@ -0,0 +1,55 @@ +################### +### Extensions #### +################### +FROM keitaro/ckan:2.9.0 as extbuild + +MAINTAINER Keitaro Inc + +# Locations and tags, please use specific tags or revisions +ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest +ENV HARVEST_GIT_BRANCH=v1.3.1 + +# Switch to the root user +USER root + +# Install necessary packages to build extensions +RUN apk add --no-cache \ + gcc \ + g++ \ + libffi-dev \ + openssl-dev \ + python3-dev + +# Fetch and build the custom CKAN extensions +RUN pip wheel --wheel-dir=/wheels git+${HARVEST_GIT_URL}@${HARVEST_GIT_BRANCH}#egg=ckanext-harvest +RUN pip wheel --wheel-dir=/wheels -r https://raw.githubusercontent.com/ckan/ckanext-harvest/${HARVEST_GIT_BRANCH}/pip-requirements.txt +RUN curl -o /wheels/harvest.txt https://raw.githubusercontent.com/ckan/ckanext-harvest/${HARVEST_GIT_BRANCH}/pip-requirements.txt + +############ +### MAIN ### +############ +FROM keitaro/ckan:2.9.0 + +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher harvest ckan_harvester + +# Switch to the root user +USER root + +COPY --from=extbuild /wheels /srv/app/ext_wheels + +# Install and enable the custom extensions +RUN pip install --no-index --find-links=/srv/app/ext_wheels ckanext-harvest && \ + pip install --no-index --find-links=/srv/app/ext_wheels -r /srv/app/ext_wheels/harvest.txt && \ + # Not working atm since ckan config tool tries to load config before executing config-tool, workaround + # ckan -c ${APP_DIR}/production.ini config-tool "ckan.plugins = ${CKAN__PLUGINS}" && \ + sed -i "/ckan.plugins = envvars/c ckan.plugins = ${CKAN__PLUGINS}" ${APP_DIR}/production.ini && \ + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/ext_wheels + +# Add harvest entrypoint script +COPY ./scripts/00_harvest.sh ${APP_DIR}/docker-entrypoint.d/00_harvest.sh + +# Switch to the ckan user +USER ckan diff --git a/examples/harvest/docker-compose.yml b/examples/harvest/docker-compose.yml new file mode 100644 index 0000000..ea38bbd --- /dev/null +++ b/examples/harvest/docker-compose.yml @@ -0,0 +1,191 @@ +# docker-compose build && docker-compose up -d +version: "3" + +volumes: + ckan_data: + pg_data: + solr_data: + +services: + ckan: + container_name: ckan + build: + context: . + networks: + - frontend + - backend + depends_on: + - db + ports: + - "0.0.0.0:${CKAN_PORT}:5000" + env_file: + - ./.ckan-env + environment: + - CKAN_SQLALCHEMY_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/ckan + - CKAN_DATASTORE_WRITE_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/datastore + - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore + - CKAN_SOLR_URL=http://solr:8983/solr/ckan + - CKAN_REDIS_URL=redis://redis:6379/1 + - CKAN_DATAPUSHER_URL=http://datapusher:8000 + - CKAN_SITE_URL=${CKAN_SITE_URL} + - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + volumes: + - ckan_data:/srv/app/data + + ckan-harvest-gather: + container_name: ckan-harvest-gather + build: + context: . + command: ckan -c /srv/app/production.ini harvester gather_consumer + restart: on-failure + networks: + - frontend + - backend + depends_on: + - db + - ckan + env_file: + - ./.ckan-env + environment: + - CKAN_SQLALCHEMY_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/ckan + - CKAN_DATASTORE_WRITE_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/datastore + - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore + - CKAN_SOLR_URL=http://solr:8983/solr/ckan + - CKAN_REDIS_URL=redis://redis:6379/1 + - CKAN_DATAPUSHER_URL=http://datapusher:8000 + - CKAN_SITE_URL=${CKAN_SITE_URL} + - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + + ckan-harvest-fetch: + container_name: ckan-harvest-fetch + build: + context: . + command: ckan -c /srv/app/production.ini harvester fetch_consumer + restart: on-failure + networks: + - frontend + - backend + depends_on: + - db + - ckan + env_file: + - ./.ckan-env + environment: + - CKAN_SQLALCHEMY_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/ckan + - CKAN_DATASTORE_WRITE_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/datastore + - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore + - CKAN_SOLR_URL=http://solr:8983/solr/ckan + - CKAN_REDIS_URL=redis://redis:6379/1 + - CKAN_DATAPUSHER_URL=http://datapusher:8000 + - CKAN_SITE_URL=${CKAN_SITE_URL} + - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + + ckan-harvest-run: + container_name: ckan-harvest-run + build: + context: . + command: /bin/sh -c "while true; do sleep 90; ckan -c /srv/app/production.ini harvester run; done" + networks: + - frontend + - backend + depends_on: + - db + - ckan + env_file: + - ./.ckan-env + environment: + - CKAN_SQLALCHEMY_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/ckan + - CKAN_DATASTORE_WRITE_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/datastore + - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore + - CKAN_SOLR_URL=http://solr:8983/solr/ckan + - CKAN_REDIS_URL=redis://redis:6379/1 + - CKAN_DATAPUSHER_URL=http://datapusher:8000 + - CKAN_SITE_URL=${CKAN_SITE_URL} + - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + + ckan-harvest-cleanlog: + container_name: ckan-harvest-cleanlog + build: + context: . + command: /bin/sh -c "while true; do sleep 150; ckan -c /srv/app/production.ini harvester clean_harvest_log; done" + networks: + - frontend + - backend + depends_on: + - db + - ckan + env_file: + - ./.ckan-env + environment: + - CKAN_SQLALCHEMY_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/ckan + - CKAN_DATASTORE_WRITE_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/datastore + - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore + - CKAN_SOLR_URL=http://solr:8983/solr/ckan + - CKAN_REDIS_URL=redis://redis:6379/1 + - CKAN_DATAPUSHER_URL=http://datapusher:8000 + - CKAN_SITE_URL=${CKAN_SITE_URL} + - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + + datapusher: + container_name: datapusher + image: keitaro/ckan-datapusher:${DATAPUSHER_VERSION} + networks: + - frontend + - backend + ports: + - "8000:8000" + environment: + - DATAPUSHER_MAX_CONTENT_LENGTH=${DATAPUSHER_MAX_CONTENT_LENGTH} + - DATAPUSHER_CHUNK_SIZE=${DATAPUSHER_CHUNK_SIZE} + - DATAPUSHER_CHUNK_INSERT_ROWS=${DATAPUSHER_CHUNK_INSERT_ROWS} + - DATAPUSHER_DOWNLOAD_TIMEOUT=${DATAPUSHER_DOWNLOAD_TIMEOUT} + + db: + container_name: db + build: + context: ../../compose + dockerfile: postgresql/Dockerfile + args: + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + networks: + - backend + environment: + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + volumes: + - pg_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "ckan"] + + solr: + container_name: solr + build: + context: ../../compose + dockerfile: solr/Dockerfile + args: + - CKAN_VERSION=${CKAN_VERSION} + networks: + - backend + volumes: + - solr_data:/opt/solr/server/solr/ckan/data + + redis: + container_name: redis + image: redis:${REDIS_VERSION} + networks: + - backend + +networks: + frontend: + backend: diff --git a/examples/harvest/scripts/00_harvest.sh b/examples/harvest/scripts/00_harvest.sh new file mode 100644 index 0000000..5eb6698 --- /dev/null +++ b/examples/harvest/scripts/00_harvest.sh @@ -0,0 +1,2 @@ +#!/bin/sh +ckan -c /srv/app/production.ini harvester initdb From f8425cb22dd9fe8ec9c3fd6d4ce116041fba0198 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 11 Sep 2020 13:36:23 +0200 Subject: [PATCH 052/213] Change default run times for harvest run and clean-log --- examples/harvest/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/harvest/docker-compose.yml b/examples/harvest/docker-compose.yml index ea38bbd..2d55864 100644 --- a/examples/harvest/docker-compose.yml +++ b/examples/harvest/docker-compose.yml @@ -90,7 +90,7 @@ services: container_name: ckan-harvest-run build: context: . - command: /bin/sh -c "while true; do sleep 90; ckan -c /srv/app/production.ini harvester run; done" + command: /bin/sh -c "while true; do sleep 900; ckan -c /srv/app/production.ini harvester run; done" networks: - frontend - backend @@ -115,7 +115,7 @@ services: container_name: ckan-harvest-cleanlog build: context: . - command: /bin/sh -c "while true; do sleep 150; ckan -c /srv/app/production.ini harvester clean_harvest_log; done" + command: /bin/sh -c "while true; do sleep 86400; ckan -c /srv/app/production.ini harvester clean_harvest_log; done" networks: - frontend - backend From 43cd280433d569169e4cd998831727bf5d869275 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Fri, 11 Sep 2020 15:01:58 +0200 Subject: [PATCH 053/213] fixes s3filestore settings --- examples/s3filestore/.ckan-env | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/s3filestore/.ckan-env b/examples/s3filestore/.ckan-env index 3b9b9ef..f3359ae 100644 --- a/examples/s3filestore/.ckan-env +++ b/examples/s3filestore/.ckan-env @@ -25,9 +25,9 @@ CKAN_SMTP_PASSWORD=pass CKAN_SMTP_MAIL_FROM=ckan@localhost # S3/MINIO settings -CKANEXT__S3FILESTORE__AWS_ACCESS_KEY_ID = MINIOACCESSKEY -CKANEXT__S3FILESTORE__AWS_SECRET_ACCESS_KEY = MINIOSECRETKEY -CKANEXT__S3FILESTORE__AWS_BUCKET_NAME = ckan -CKANEXT__S3FILESTORE__HOST_NAME = http://minio:9000 -CKANEXT__S3FILESTORE__REGION_NAME = us-east-1 -CKANEXT__S3FILESTORE__SIGNATURE_VERSION = s3v4 +CKANEXT__S3FILESTORE__AWS_ACCESS_KEY_ID=MINIOACCESSKEY +CKANEXT__S3FILESTORE__AWS_SECRET_ACCESS_KEY=MINIOSECRETKEY +CKANEXT__S3FILESTORE__AWS_BUCKET_NAME=ckan +CKANEXT__S3FILESTORE__HOST_NAME=http://minio:9000 +CKANEXT__S3FILESTORE__REGION_NAME=us-east-1 +CKANEXT__S3FILESTORE__SIGNATURE_VERSION=s3v4 From 6ca163fae635e20b7b02627c2d431da210b14b4a Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Mon, 14 Sep 2020 16:33:21 +0200 Subject: [PATCH 054/213] Fix zookeeper returning true when in SolrCloud mode --- images/ckan/2.7/setup/app/prerun.py | 2 ++ images/ckan/2.8/setup/app/prerun.py | 2 ++ images/ckan/2.9/setup/app/prerun.py | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/images/ckan/2.7/setup/app/prerun.py b/images/ckan/2.7/setup/app/prerun.py index 0dbd70e..5a19d3f 100644 --- a/images/ckan/2.7/setup/app/prerun.py +++ b/images/ckan/2.7/setup/app/prerun.py @@ -57,6 +57,8 @@ def check_solr_connection(retry=None): else: import re conn_info = connection.read() + # SolrCloud + conn_info = re.sub(r'"zkConnected":true', '"zkConnected":True', conn_info) eval(conn_info) def init_db(): diff --git a/images/ckan/2.8/setup/app/prerun.py b/images/ckan/2.8/setup/app/prerun.py index 0dbd70e..5a19d3f 100644 --- a/images/ckan/2.8/setup/app/prerun.py +++ b/images/ckan/2.8/setup/app/prerun.py @@ -57,6 +57,8 @@ def check_solr_connection(retry=None): else: import re conn_info = connection.read() + # SolrCloud + conn_info = re.sub(r'"zkConnected":true', '"zkConnected":True', conn_info) eval(conn_info) def init_db(): diff --git a/images/ckan/2.9/setup/app/prerun.py b/images/ckan/2.9/setup/app/prerun.py index 01e766e..f2ce383 100644 --- a/images/ckan/2.9/setup/app/prerun.py +++ b/images/ckan/2.9/setup/app/prerun.py @@ -57,7 +57,8 @@ def check_solr_connection(retry=None): else: import re conn_info = connection.read() - print(conn_info) + # SolrCloud + conn_info = re.sub(r'"zkConnected":true', '"zkConnected":True', conn_info) eval(conn_info) def init_db(): From bb3e1d401c66480798da7ae132bb4d8c7abfec1e Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 16 Sep 2020 09:27:06 +0200 Subject: [PATCH 055/213] Decode in utf-8 solr check in prerun conn_info --- images/ckan/2.9/setup/app/prerun.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/ckan/2.9/setup/app/prerun.py b/images/ckan/2.9/setup/app/prerun.py index f2ce383..60b22d6 100644 --- a/images/ckan/2.9/setup/app/prerun.py +++ b/images/ckan/2.9/setup/app/prerun.py @@ -58,7 +58,7 @@ def check_solr_connection(retry=None): import re conn_info = connection.read() # SolrCloud - conn_info = re.sub(r'"zkConnected":true', '"zkConnected":True', conn_info) + conn_info = re.sub(r'"zkConnected":true', '"zkConnected":True', conn_info.decode('utf-8')) eval(conn_info) def init_db(): From 4e0634fca7637f8392e26f7a1b6fbcc8c41aaaee Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Sun, 20 Sep 2020 00:03:36 +0200 Subject: [PATCH 056/213] Add ckan patch script and patches as part of the ckan image build process --- compose/.ckan-env | 4 ++++ compose/.env | 3 +++ compose/docker-compose.yml | 5 ++++- examples/harvest/.ckan-env | 4 ++++ examples/harvest/.env | 3 +++ examples/harvest/docker-compose.yml | 8 +++----- examples/s3filestore/.ckan-env | 4 ++++ examples/s3filestore/.env | 3 +++ examples/s3filestore/docker-compose.yml | 4 +++- images/ckan/2.7/Dockerfile | 6 ++++++ .../00_datapusher_fix_callback_url.patch | 18 ++++++++++++++++++ images/ckan/2.7/scripts/apply_ckan_patches.sh | 5 +++++ images/ckan/2.8/Dockerfile | 6 ++++++ .../00_datapusher_fix_callback_url.patch | 18 ++++++++++++++++++ images/ckan/2.8/scripts/apply_ckan_patches.sh | 5 +++++ images/ckan/2.9/Dockerfile | 6 ++++++ images/ckan/2.9/scripts/apply_ckan_patches.sh | 5 +++++ images/datapusher/Dockerfile | 2 +- images/datapusher/setup/datapusher_settings.py | 4 ++++ 19 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 images/ckan/2.7/patches/00_datapusher_fix_callback_url.patch create mode 100755 images/ckan/2.7/scripts/apply_ckan_patches.sh create mode 100644 images/ckan/2.8/patches/00_datapusher_fix_callback_url.patch create mode 100755 images/ckan/2.8/scripts/apply_ckan_patches.sh create mode 100755 images/ckan/2.9/scripts/apply_ckan_patches.sh diff --git a/compose/.ckan-env b/compose/.ckan-env index 344ee7d..59e99bd 100644 --- a/compose/.ckan-env +++ b/compose/.ckan-env @@ -23,3 +23,7 @@ CKAN_SMTP_STARTTLS=True CKAN_SMTP_USER=user CKAN_SMTP_PASSWORD=pass CKAN_SMTP_MAIL_FROM=ckan@localhost + +# Datapusher configuration +CKAN__DATAPUSHER__URL=http://datapusher:8000 +CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000/ diff --git a/compose/.env b/compose/.env index 89b30ec..a8a815e 100644 --- a/compose/.env +++ b/compose/.env @@ -24,6 +24,9 @@ DATAPUSHER_MAX_CONTENT_LENGTH=10485760 DATAPUSHER_CHUNK_SIZE=16384 DATAPUSHER_CHUNK_INSERT_ROWS=250 DATAPUSHER_DOWNLOAD_TIMEOUT=30 +DATAPUSHER_SSL_VERIFY=False +DATAPUSHER_REWRITE_RESOURCES=True +DATAPUSHER_REWRITE_URL=http://ckan:5000 # Redis REDIS_VERSION=6.0.7 diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index f8f48c4..675fbf8 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -25,7 +25,6 @@ services: - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore - CKAN_SOLR_URL=http://solr:8983/solr/ckan - CKAN_REDIS_URL=redis://redis:6379/1 - - CKAN_DATAPUSHER_URL=http://datapusher:8000 - CKAN_SITE_URL=${CKAN_SITE_URL} - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} @@ -46,6 +45,10 @@ services: - DATAPUSHER_CHUNK_SIZE=${DATAPUSHER_CHUNK_SIZE} - DATAPUSHER_CHUNK_INSERT_ROWS=${DATAPUSHER_CHUNK_INSERT_ROWS} - DATAPUSHER_DOWNLOAD_TIMEOUT=${DATAPUSHER_DOWNLOAD_TIMEOUT} + - DATAPUSHER_SSL_VERIFY=${DATA_PUSHER_SSL_VERIFY} + - DATAPUSHER_REWRITE_RESOURCES=${DATAPUSHER_REWRITE_RESOURCES} + - DATAPUSHER_REWRITE_URL=${DATAPUSHER_REWRITE_URL} + db: container_name: db diff --git a/examples/harvest/.ckan-env b/examples/harvest/.ckan-env index c1a0993..3801b91 100644 --- a/examples/harvest/.ckan-env +++ b/examples/harvest/.ckan-env @@ -24,6 +24,10 @@ CKAN_SMTP_USER=user CKAN_SMTP_PASSWORD=pass CKAN_SMTP_MAIL_FROM=ckan@localhost +# Datapusher configuration +CKAN__DATAPUSHER__URL=http://datapusher:8000 +CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000/ + # Harvest settings CKAN__HARVEST__MQ__TYPE=redis CKAN__HARVEST__MQ__HOSTNAME=redis diff --git a/examples/harvest/.env b/examples/harvest/.env index 89b30ec..a8a815e 100644 --- a/examples/harvest/.env +++ b/examples/harvest/.env @@ -24,6 +24,9 @@ DATAPUSHER_MAX_CONTENT_LENGTH=10485760 DATAPUSHER_CHUNK_SIZE=16384 DATAPUSHER_CHUNK_INSERT_ROWS=250 DATAPUSHER_DOWNLOAD_TIMEOUT=30 +DATAPUSHER_SSL_VERIFY=False +DATAPUSHER_REWRITE_RESOURCES=True +DATAPUSHER_REWRITE_URL=http://ckan:5000 # Redis REDIS_VERSION=6.0.7 diff --git a/examples/harvest/docker-compose.yml b/examples/harvest/docker-compose.yml index 2d55864..fc2f961 100644 --- a/examples/harvest/docker-compose.yml +++ b/examples/harvest/docker-compose.yml @@ -26,7 +26,6 @@ services: - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore - CKAN_SOLR_URL=http://solr:8983/solr/ckan - CKAN_REDIS_URL=redis://redis:6379/1 - - CKAN_DATAPUSHER_URL=http://datapusher:8000 - CKAN_SITE_URL=${CKAN_SITE_URL} - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} @@ -54,7 +53,6 @@ services: - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore - CKAN_SOLR_URL=http://solr:8983/solr/ckan - CKAN_REDIS_URL=redis://redis:6379/1 - - CKAN_DATAPUSHER_URL=http://datapusher:8000 - CKAN_SITE_URL=${CKAN_SITE_URL} - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} @@ -80,7 +78,6 @@ services: - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore - CKAN_SOLR_URL=http://solr:8983/solr/ckan - CKAN_REDIS_URL=redis://redis:6379/1 - - CKAN_DATAPUSHER_URL=http://datapusher:8000 - CKAN_SITE_URL=${CKAN_SITE_URL} - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} @@ -105,7 +102,6 @@ services: - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore - CKAN_SOLR_URL=http://solr:8983/solr/ckan - CKAN_REDIS_URL=redis://redis:6379/1 - - CKAN_DATAPUSHER_URL=http://datapusher:8000 - CKAN_SITE_URL=${CKAN_SITE_URL} - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} @@ -130,7 +126,6 @@ services: - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore - CKAN_SOLR_URL=http://solr:8983/solr/ckan - CKAN_REDIS_URL=redis://redis:6379/1 - - CKAN_DATAPUSHER_URL=http://datapusher:8000 - CKAN_SITE_URL=${CKAN_SITE_URL} - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} @@ -149,6 +144,9 @@ services: - DATAPUSHER_CHUNK_SIZE=${DATAPUSHER_CHUNK_SIZE} - DATAPUSHER_CHUNK_INSERT_ROWS=${DATAPUSHER_CHUNK_INSERT_ROWS} - DATAPUSHER_DOWNLOAD_TIMEOUT=${DATAPUSHER_DOWNLOAD_TIMEOUT} + - DATAPUSHER_SSL_VERIFY=${DATAPUSHER_SSL_VERIFY} + - DATAPUSHER_REWRITE_RESOURCES=${DATAPUSHER_REWRITE_RESOURCES} + - DATAPUSHER_REWRITE_URL=${DATAPUSHER_REWRITE_URL} db: container_name: db diff --git a/examples/s3filestore/.ckan-env b/examples/s3filestore/.ckan-env index f3359ae..95e1820 100644 --- a/examples/s3filestore/.ckan-env +++ b/examples/s3filestore/.ckan-env @@ -24,6 +24,10 @@ CKAN_SMTP_USER=user CKAN_SMTP_PASSWORD=pass CKAN_SMTP_MAIL_FROM=ckan@localhost +# Datapusher configuration +CKAN__DATAPUSHER__URL=http://datapusher:8000 +CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000/ + # S3/MINIO settings CKANEXT__S3FILESTORE__AWS_ACCESS_KEY_ID=MINIOACCESSKEY CKANEXT__S3FILESTORE__AWS_SECRET_ACCESS_KEY=MINIOSECRETKEY diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env index 4068526..542a31a 100644 --- a/examples/s3filestore/.env +++ b/examples/s3filestore/.env @@ -24,6 +24,9 @@ DATAPUSHER_MAX_CONTENT_LENGTH=10485760 DATAPUSHER_CHUNK_SIZE=16384 DATAPUSHER_CHUNK_INSERT_ROWS=250 DATAPUSHER_DOWNLOAD_TIMEOUT=30 +DATAPUSHER_SSL_VERIFY=False +DATAPUSHER_REWRITE_RESOURCES=True +DATAPUSHER_REWRITE_URL=http://ckan:5000 # Redis REDIS_VERSION=6.0.7 diff --git a/examples/s3filestore/docker-compose.yml b/examples/s3filestore/docker-compose.yml index d195fbb..a2eb487 100644 --- a/examples/s3filestore/docker-compose.yml +++ b/examples/s3filestore/docker-compose.yml @@ -26,7 +26,6 @@ services: - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore - CKAN_SOLR_URL=http://solr:8983/solr/ckan - CKAN_REDIS_URL=redis://redis:6379/1 - - CKAN_DATAPUSHER_URL=http://datapusher:8000 - CKAN_SITE_URL=${CKAN_SITE_URL} - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} @@ -45,6 +44,9 @@ services: - DATAPUSHER_CHUNK_SIZE=${DATAPUSHER_CHUNK_SIZE} - DATAPUSHER_CHUNK_INSERT_ROWS=${DATAPUSHER_CHUNK_INSERT_ROWS} - DATAPUSHER_DOWNLOAD_TIMEOUT=${DATAPUSHER_DOWNLOAD_TIMEOUT} + - DATAPUSHER_SSL_VERIFY=${DATAPUSHER_SSL_VERIFY} + - DATAPUSHER_REWRITE_RESOURCES=${DATAPUSHER_REWRITE_RESOURCES} + - DATAPUSHER_REWRITE_URL=${DATAPUSHER_REWRITE_URL} db: container_name: db diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 74878a7..ffdcd9f 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -26,6 +26,7 @@ RUN apk add --no-cache \ autoconf \ automake \ libtool \ + patch \ musl-dev \ pcre-dev \ pcre \ @@ -47,6 +48,11 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan +# Copy patches and apply patches script +COPY ./patches ${SRC_DIR}/patches +COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh +# Apply patches +RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 diff --git a/images/ckan/2.7/patches/00_datapusher_fix_callback_url.patch b/images/ckan/2.7/patches/00_datapusher_fix_callback_url.patch new file mode 100644 index 0000000..83d03d5 --- /dev/null +++ b/images/ckan/2.7/patches/00_datapusher_fix_callback_url.patch @@ -0,0 +1,18 @@ +--- ckan/ckanext/datapusher/logic/action.py 2020-09-19 21:25:06.525350202 +0200 ++++ action_fix_callback_url.py 2020-09-19 21:27:43.596501963 +0200 +@@ -61,13 +61,13 @@ + + datapusher_url = config.get('ckan.datapusher.url') + +- site_url = h.url_for('/', qualified=True) +- + callback_url_base = config.get('ckan.datapusher.callback_url_base') + if callback_url_base: ++ site_url = callback_url_base + callback_url = urlparse.urljoin( + callback_url_base.rstrip('/'), '/api/3/action/datapusher_hook') + else: ++ site_url = h.url_for('/', qualified=True) + callback_url = h.url_for( + '/api/3/action/datapusher_hook', qualified=True) + diff --git a/images/ckan/2.7/scripts/apply_ckan_patches.sh b/images/ckan/2.7/scripts/apply_ckan_patches.sh new file mode 100755 index 0000000..a7bceb9 --- /dev/null +++ b/images/ckan/2.7/scripts/apply_ckan_patches.sh @@ -0,0 +1,5 @@ +#!/bin/bash +shopt -s nullglob +for patch in patches/*.patch; do + /usr/bin/patch -p0 -i $patch +done diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index ad29d5d..028a097 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -25,6 +25,7 @@ RUN apk add --no-cache \ g++ \ autoconf \ automake \ + patch \ libtool \ musl-dev \ pcre-dev \ @@ -43,6 +44,11 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan +# Copy patches and apply patches script +COPY ./patches ${SRC_DIR}/patches +COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh +# Apply patches +RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 diff --git a/images/ckan/2.8/patches/00_datapusher_fix_callback_url.patch b/images/ckan/2.8/patches/00_datapusher_fix_callback_url.patch new file mode 100644 index 0000000..83d03d5 --- /dev/null +++ b/images/ckan/2.8/patches/00_datapusher_fix_callback_url.patch @@ -0,0 +1,18 @@ +--- ckan/ckanext/datapusher/logic/action.py 2020-09-19 21:25:06.525350202 +0200 ++++ action_fix_callback_url.py 2020-09-19 21:27:43.596501963 +0200 +@@ -61,13 +61,13 @@ + + datapusher_url = config.get('ckan.datapusher.url') + +- site_url = h.url_for('/', qualified=True) +- + callback_url_base = config.get('ckan.datapusher.callback_url_base') + if callback_url_base: ++ site_url = callback_url_base + callback_url = urlparse.urljoin( + callback_url_base.rstrip('/'), '/api/3/action/datapusher_hook') + else: ++ site_url = h.url_for('/', qualified=True) + callback_url = h.url_for( + '/api/3/action/datapusher_hook', qualified=True) + diff --git a/images/ckan/2.8/scripts/apply_ckan_patches.sh b/images/ckan/2.8/scripts/apply_ckan_patches.sh new file mode 100755 index 0000000..a7bceb9 --- /dev/null +++ b/images/ckan/2.8/scripts/apply_ckan_patches.sh @@ -0,0 +1,5 @@ +#!/bin/bash +shopt -s nullglob +for patch in patches/*.patch; do + /usr/bin/patch -p0 -i $patch +done diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index c65125b..c4f2dc9 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -26,6 +26,7 @@ RUN apk add --no-cache \ autoconf \ automake \ libtool \ + patch \ musl-dev \ pcre-dev \ pcre \ @@ -49,6 +50,11 @@ RUN pip install setuptools==44.1.0 # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan +# Copy patches and apply patches script +COPY ./patches ${SRC_DIR}/patches +COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh +# Apply patches +RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 diff --git a/images/ckan/2.9/scripts/apply_ckan_patches.sh b/images/ckan/2.9/scripts/apply_ckan_patches.sh new file mode 100755 index 0000000..a7bceb9 --- /dev/null +++ b/images/ckan/2.9/scripts/apply_ckan_patches.sh @@ -0,0 +1,5 @@ +#!/bin/bash +shopt -s nullglob +for patch in patches/*.patch; do + /usr/bin/patch -p0 -i $patch +done diff --git a/images/datapusher/Dockerfile b/images/datapusher/Dockerfile index 5bff8c4..1428b2d 100644 --- a/images/datapusher/Dockerfile +++ b/images/datapusher/Dockerfile @@ -5,7 +5,7 @@ FROM alpine:3.12 as build # Set datapusher version to build ENV GIT_URL https://github.com/keitaroinc/datapusher.git -ENV GIT_BRANCH parametrize-job-config +ENV GIT_BRANCH master ENV REQUIREMENTS_URL https://raw.githubusercontent.com/keitaroinc/datapusher/${GIT_BRANCH}/requirements.txt # Set src dirs diff --git a/images/datapusher/setup/datapusher_settings.py b/images/datapusher/setup/datapusher_settings.py index cf0a138..17e267c 100644 --- a/images/datapusher/setup/datapusher_settings.py +++ b/images/datapusher/setup/datapusher_settings.py @@ -31,3 +31,7 @@ MAX_CONTENT_LENGTH = int(os.environ.get('DATAPUSHER_MAX_CONTENT_LENGTH', '102400 CHUNK_SIZE = int(os.environ.get('DATAPUSHER_CHUNK_SIZE', '16384')) CHUNK_INSERT_ROWS = int(os.environ.get('DATAPUSHER_CHUNK_INSERT_ROWS', '250')) DOWNLOAD_TIMEOUT = int(os.environ.get('DATAPUSHER_DOWNLOAD_TIMEOUT', '30')) + +# Rewrite resource URL's when ckan callback url base is used +REWRITE_RESOURCES = os.environ.get('DATAPUSHER_REWRITE_RESOURCES', False) +REWRITE_URL = os.environ.get('DATAPUSHER_REWRITE_URL', 'http://ckan:5000/') From d0f2d89bbf445adc0bacc352c0dd7b6d31d7a2f5 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Sun, 20 Sep 2020 00:45:46 +0200 Subject: [PATCH 057/213] Add after prerun/init scripts --- examples/harvest/Dockerfile | 4 ++-- .../harvest/{scripts => afterinit.d}/00_harvest.sh | 0 images/ckan/2.7/Dockerfile | 3 +++ images/ckan/2.7/setup/app/start_ckan.sh | 13 +++++++++++++ images/ckan/2.8/Dockerfile | 3 +++ images/ckan/2.8/setup/app/start_ckan.sh | 13 +++++++++++++ images/ckan/2.9/Dockerfile | 3 +++ images/ckan/2.9/setup/app/start_ckan.sh | 13 +++++++++++++ 8 files changed, 50 insertions(+), 2 deletions(-) rename examples/harvest/{scripts => afterinit.d}/00_harvest.sh (100%) diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index f12a33d..20e3131 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -48,8 +48,8 @@ RUN pip install --no-index --find-links=/srv/app/ext_wheels ckanext-harvest && \ # Remove wheels RUN rm -rf /srv/app/ext_wheels -# Add harvest entrypoint script -COPY ./scripts/00_harvest.sh ${APP_DIR}/docker-entrypoint.d/00_harvest.sh +# Add harvest afterinit script +COPY ./afterinit.d/00_harvest.sh ${APP_DIR}/docker-afterinit.d/00_harvest.sh # Switch to the ckan user USER ckan diff --git a/examples/harvest/scripts/00_harvest.sh b/examples/harvest/afterinit.d/00_harvest.sh similarity index 100% rename from examples/harvest/scripts/00_harvest.sh rename to examples/harvest/afterinit.d/00_harvest.sh diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index ffdcd9f..bacab4d 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -167,6 +167,9 @@ COPY setup/app ${APP_DIR} # Create entrypoint directory for children image scripts ONBUILD RUN mkdir docker-entrypoint.d +# Create afterinit directory for children image scripts +ONBUILD RUN mkdir docker-afterinit.d + EXPOSE 5000 HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 diff --git a/images/ckan/2.7/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh index f870cb8..722783a 100755 --- a/images/ckan/2.7/setup/app/start_ckan.sh +++ b/images/ckan/2.7/setup/app/start_ckan.sh @@ -18,6 +18,19 @@ UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --e # Run the prerun script to init CKAN and create the default admin user python prerun.py +# Run any after prerun/init scripts provided by images extending this one +if [[ -d "${APP_DIR}/docker-afterinit.d" ]] +then + for f in ${APP_DIR}/docker-afterinit.d/*; do + case "$f" in + *.sh) echo "$0: Running after prerun init file $f"; . "$f" ;; + *.py) echo "$0: Running after prerun init file $f"; python "$f"; echo ;; + *) echo "$0: Ignoring $f (not an sh or py file)" ;; + esac + echo + done +fi + # Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully if [ $? -eq 0 ] then diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 028a097..dda291a 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -161,6 +161,9 @@ COPY setup/app ${APP_DIR} # Create entrypoint directory for children image scripts ONBUILD RUN mkdir docker-entrypoint.d +# Create afterinit directory for children image scripts +ONBUILD RUN mkdir docker-afterinit.d + EXPOSE 5000 HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 diff --git a/images/ckan/2.8/setup/app/start_ckan.sh b/images/ckan/2.8/setup/app/start_ckan.sh index f870cb8..722783a 100755 --- a/images/ckan/2.8/setup/app/start_ckan.sh +++ b/images/ckan/2.8/setup/app/start_ckan.sh @@ -18,6 +18,19 @@ UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --e # Run the prerun script to init CKAN and create the default admin user python prerun.py +# Run any after prerun/init scripts provided by images extending this one +if [[ -d "${APP_DIR}/docker-afterinit.d" ]] +then + for f in ${APP_DIR}/docker-afterinit.d/*; do + case "$f" in + *.sh) echo "$0: Running after prerun init file $f"; . "$f" ;; + *.py) echo "$0: Running after prerun init file $f"; python "$f"; echo ;; + *) echo "$0: Ignoring $f (not an sh or py file)" ;; + esac + echo + done +fi + # Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully if [ $? -eq 0 ] then diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index c4f2dc9..63ca7f8 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -176,6 +176,9 @@ COPY setup/app ${APP_DIR} # Create entrypoint directory for children image scripts ONBUILD RUN mkdir docker-entrypoint.d +# Create afterinit directory for children image scripts +ONBUILD RUN mkdir docker-afterinit.d + EXPOSE 5000 HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index e8f67ae..fee6687 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -18,6 +18,19 @@ UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid ckan --gid ckan --http :5000 --master # Run the prerun script to init CKAN and create the default admin user python prerun.py +# Run any after prerun/init scripts provided by images extending this one +if [[ -d "${APP_DIR}/docker-afterinit.d" ]] +then + for f in ${APP_DIR}/docker-afterinit.d/*; do + case "$f" in + *.sh) echo "$0: Running after prerun init file $f"; . "$f" ;; + *.py) echo "$0: Running after prerun init file $f"; python "$f"; echo ;; + *) echo "$0: Ignoring $f (not an sh or py file)" ;; + esac + echo + done +fi + # Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully if [ $? -eq 0 ] then From df5f8dc45891037a1fa5cb121e9a9270ec36de4c Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Mon, 21 Sep 2020 08:51:52 +0200 Subject: [PATCH 058/213] Update readme for afterinit.d usage --- Readme.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 584ce2a..b1bc213 100644 --- a/Readme.md +++ b/Readme.md @@ -73,8 +73,10 @@ RUN rm -rf /srv/app/ext_wheels USER ckan ``` -### Adding prerun scripts -You can add scripts to CKAN custom images and copy them to the *docker-entrypoint.d* directory. Any *.sh or *.py file in that directory will be executed after the main initialization script (prerun.py) is executed. +### Adding init and afterinit scripts +You can add scripts to CKAN custom images and copy them to the *docker-entrypoint.d* directory. Any *.sh or *.py file in that directory will be executed before the main initialization script (prerun.py) is executed. + +You can add scripts to CKAN custom images and copy them to the *docker-afterinit.d* directory. Any *.sh or *.py file in that directory will be executed after the main initialization script (prerun.py) is executed. ## Build To build a CKAN image run: From 9459fff45836ac2c46c0f9b3291ea0463dde66cb Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 23 Sep 2020 08:42:40 +0200 Subject: [PATCH 059/213] Add license --- LICENSE | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..725a7a7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2020 Keitaro AB + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From a4de7f7f23ea72edd9e682674bf3383bf094e998 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 23 Sep 2020 08:46:05 +0200 Subject: [PATCH 060/213] Add Code of Conduct --- CODE_OF_CONDUCT.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..04b029f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at info@keitaro.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq From 2791eb2545927daee3d6d81728e6f7c70460d0a2 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 23 Sep 2020 08:51:51 +0200 Subject: [PATCH 061/213] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 21 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..da1cf2d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Docker images affected** +1. CKAN 2.8 +2. CKAN 2.9 + +**Expected behaviour** +A clear and concise description of what you expected to happen. + +**Logs** +If applicable, add logs to help explain your problem. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..fe47f95 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE]" +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context about the feature request here. From dd0a141dfeec0bbf4e38380c7ec8b26089f0bd84 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 23 Sep 2020 09:01:05 +0200 Subject: [PATCH 062/213] Add Contributing Guidelines --- CONTRIBUTING.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7edd28e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +## How to contribute to Dockerized CKAN + +#### **Did you find a bug?** + +* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/keitaroinc/docker-ckan/issues). + +* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/keitaroinc/docker-ckan/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, we include an issue template to help out in filling-in the issue. + +#### **Did you write a patch that fixes a bug?** + +* Open a new GitHub pull request with the patch. + +* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. + +#### **Do you intend to add a new feature or change an existing one?** + +* [Create a new feature issue](https://github.com/keitaroinc/docker-ckan/issues/new) using the Feature Request template and describe your proposed changes + +* Submit a pull request referring to the relevant feature issue/s + +#### **Do you have questions about the source code?** + +* Ask any question about how to use Dockerized CKAN in our [gitter chat](https://gitter.im/keitaroinc/docker-ckan). + +Thanks! + +Keitaro Team From b3123ed40b175eba5a8e78df545538181eb7220f Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 23 Sep 2020 10:00:07 +0200 Subject: [PATCH 063/213] Add shield badges --- Readme.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index b1bc213..1cea446 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,7 @@ -# Dockerized CKAN ![Docker Pulls](https://img.shields.io/docker/pulls/keitaro/ckan.svg) +# Dockerized CKAN + +[![License][]][1] [![Docker Pulls][]][2] [![Chat on Gitter][]][3] + This repository contains base docker images, examples and docker-compose used to build and run CKAN. We build and publish docker images built using this repository to Dockerhub: @@ -93,3 +96,10 @@ To upload the image to DockerHub run: ```sh docker push [options] /ckan: ``` + + [License]: https://img.shields.io/badge/license-Apache--2.0-blue.svg?style=flat + [1]: https://opensource.org/licenses/Apache-2.0 + [Docker Pulls]: https://img.shields.io/docker/pulls/keitaro/ckan.svg?style=flat + [2]: https://hub.docker.com/r/keitaro/ckan + [Chat on Gitter]: https://badges.gitter.im/gitterHQ/gitter.svg + [3]: https://gitter.im/keitaroinc/docker-ckan From eb6df55058a13cbf9f183c039fb123df0690865e Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 25 Sep 2020 08:05:26 +0200 Subject: [PATCH 064/213] Pin greenlet version to 0.4.16 --- images/datapusher/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/datapusher/Dockerfile b/images/datapusher/Dockerfile index 1428b2d..d749297 100644 --- a/images/datapusher/Dockerfile +++ b/images/datapusher/Dockerfile @@ -45,7 +45,7 @@ RUN pip wheel --wheel-dir=/wheels -r ${REQUIREMENTS_URL} RUN curl -o /wheels/requirements.txt ${REQUIREMENTS_URL} # Get uwsgi and gevent from pip -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 ############ From e438bb70f3068cec5a380de12d1d57743c0bb9dd Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Sun, 27 Sep 2020 13:00:48 +0200 Subject: [PATCH 065/213] Fix ckan config-tool in 2.9 and pin greenlet --- examples/harvest/Dockerfile | 5 ++--- images/ckan/2.7/Dockerfile | 2 +- images/ckan/2.8/Dockerfile | 2 +- images/ckan/2.9/Dockerfile | 9 ++++----- images/ckan/2.9/patches/.keep | 0 images/datapusher/setup/datapusher_settings.py | 3 +++ 6 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 images/ckan/2.9/patches/.keep diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index 20e3131..47aaa55 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -40,9 +40,8 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels # Install and enable the custom extensions RUN pip install --no-index --find-links=/srv/app/ext_wheels ckanext-harvest && \ pip install --no-index --find-links=/srv/app/ext_wheels -r /srv/app/ext_wheels/harvest.txt && \ - # Not working atm since ckan config tool tries to load config before executing config-tool, workaround - # ckan -c ${APP_DIR}/production.ini config-tool "ckan.plugins = ${CKAN__PLUGINS}" && \ - sed -i "/ckan.plugins = envvars/c ckan.plugins = ${CKAN__PLUGINS}" ${APP_DIR}/production.ini && \ + # Configure plugins + ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ chown -R ckan:ckan /srv/app # Remove wheels diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index bacab4d..dd56eb4 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -55,7 +55,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 ########################### diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index dda291a..3815cae 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -51,7 +51,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 ########################### diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 63ca7f8..ba0487a 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -57,7 +57,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 ########################### @@ -157,13 +157,12 @@ RUN pip install -e /srv/app/src/ckan && \ echo "UTC" > /etc/timezone && \ # Generate CKAN config ckan generate config ${APP_DIR}/production.ini && \ - # Not working atm since ckan config tool tries to load config before executing config-tool, workaround - # ckan -c ${APP_DIR}/production.ini config-tool "ckan.plugins = ${CKAN__PLUGINS}" && \ - sed -i "/ckan.plugins = stats/c ckan.plugins = ${CKAN__PLUGINS}" ${APP_DIR}/production.ini && \ + # Configure plugins + ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Create the data directory mkdir ${DATA_DIR} && \ # Webassets can't be loaded from env variables at runtime, it needs to be in the config so that it is created - sed -i "/ckan.webassets.path = /c ckan.webassets.path = ${DATA_DIR}/webassets" ${APP_DIR}/production.ini && \ + ckan config-tool ${APP_DIR}/production.ini "ckan.webassets.path = ${DATA_DIR}/webassets" && \ # Change ownership to app user chown -R ckan:ckan /srv/app diff --git a/images/ckan/2.9/patches/.keep b/images/ckan/2.9/patches/.keep new file mode 100644 index 0000000..e69de29 diff --git a/images/datapusher/setup/datapusher_settings.py b/images/datapusher/setup/datapusher_settings.py index 17e267c..8a4413a 100644 --- a/images/datapusher/setup/datapusher_settings.py +++ b/images/datapusher/setup/datapusher_settings.py @@ -32,6 +32,9 @@ CHUNK_SIZE = int(os.environ.get('DATAPUSHER_CHUNK_SIZE', '16384')) CHUNK_INSERT_ROWS = int(os.environ.get('DATAPUSHER_CHUNK_INSERT_ROWS', '250')) DOWNLOAD_TIMEOUT = int(os.environ.get('DATAPUSHER_DOWNLOAD_TIMEOUT', '30')) +# Verify SSL +SSL_VERIFY = os.environ.get('DATAPUSHER_SSL_VERIFY', False) + # Rewrite resource URL's when ckan callback url base is used REWRITE_RESOURCES = os.environ.get('DATAPUSHER_REWRITE_RESOURCES', False) REWRITE_URL = os.environ.get('DATAPUSHER_REWRITE_URL', 'http://ckan:5000/') From 16dc43e7fd45156dc2d60450674643f141da9fd4 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 14 Oct 2020 11:03:37 +0200 Subject: [PATCH 066/213] Add dictization_functions unflatten patch --- .../01_dictization_functions_unflatten.patch | 32 +++++++++++++++++++ .../01_dictization_functions_unflatten.patch | 32 +++++++++++++++++++ .../01_dictization_functions_unflatten.patch | 32 +++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 images/ckan/2.7/patches/01_dictization_functions_unflatten.patch create mode 100644 images/ckan/2.8/patches/01_dictization_functions_unflatten.patch create mode 100644 images/ckan/2.9/patches/01_dictization_functions_unflatten.patch diff --git a/images/ckan/2.7/patches/01_dictization_functions_unflatten.patch b/images/ckan/2.7/patches/01_dictization_functions_unflatten.patch new file mode 100644 index 0000000..51258cd --- /dev/null +++ b/images/ckan/2.7/patches/01_dictization_functions_unflatten.patch @@ -0,0 +1,32 @@ +--- ckan/ckan/lib/navl/dictization_functions.py 2020-10-14 10:48:40.146291362 +0200 ++++ dictization_functions_unflatten_patch.py 2020-10-14 10:48:29.979771608 +0200 +@@ -440,19 +440,24 @@ + for flattend_key in sorted(data.keys(), key=flattened_order_key): + current_pos = unflattened + ++ if (len(flattend_key) > 1 ++ and not flattend_key[0] in convert_to_list ++ and not flattend_key[0] in unflattened): ++ convert_to_list.append(flattend_key[0]) ++ + for key in flattend_key[:-1]: + try: + current_pos = current_pos[key] +- except IndexError: +- new_pos = {} +- current_pos.append(new_pos) +- current_pos = new_pos + except KeyError: +- new_pos = [] ++ new_pos = {} + current_pos[key] = new_pos + current_pos = new_pos + current_pos[flattend_key[-1]] = data[flattend_key] + ++ for key in convert_to_list: ++ unflattened[key] = [unflattened[key][s] ++ for s in sorted(unflattened[key])] ++ + return unflattened + + diff --git a/images/ckan/2.8/patches/01_dictization_functions_unflatten.patch b/images/ckan/2.8/patches/01_dictization_functions_unflatten.patch new file mode 100644 index 0000000..573cffd --- /dev/null +++ b/images/ckan/2.8/patches/01_dictization_functions_unflatten.patch @@ -0,0 +1,32 @@ +--- ckan/ckan/lib/navl/dictization_functions.py 2020-10-14 10:39:46.319132692 +0200 ++++ dictization_functions_unflatten_patch.py 2020-10-14 10:40:38.204657981 +0200 +@@ -442,19 +442,24 @@ + for flattend_key in sorted(data.keys(), key=flattened_order_key): + current_pos = unflattened + ++ if (len(flattend_key) > 1 ++ and not flattend_key[0] in convert_to_list ++ and not flattend_key[0] in unflattened): ++ convert_to_list.append(flattend_key[0]) ++ + for key in flattend_key[:-1]: + try: + current_pos = current_pos[key] +- except IndexError: +- new_pos = {} +- current_pos.append(new_pos) +- current_pos = new_pos + except KeyError: +- new_pos = [] ++ new_pos = {} + current_pos[key] = new_pos + current_pos = new_pos + current_pos[flattend_key[-1]] = data[flattend_key] + ++ for key in convert_to_list: ++ unflattened[key] = [unflattened[key][s] ++ for s in sorted(unflattened[key])] ++ + return unflattened + + diff --git a/images/ckan/2.9/patches/01_dictization_functions_unflatten.patch b/images/ckan/2.9/patches/01_dictization_functions_unflatten.patch new file mode 100644 index 0000000..c4acfc3 --- /dev/null +++ b/images/ckan/2.9/patches/01_dictization_functions_unflatten.patch @@ -0,0 +1,32 @@ +--- ckan/ckan/lib/navl/dictization_functions.py 2020-10-14 10:45:07.519582188 +0200 ++++ dictization_functions_unflatten_patch.py 2020-10-14 10:46:26.258305671 +0200 +@@ -410,19 +410,24 @@ + for flattend_key in sorted(data.keys(), key=flattened_order_key): + current_pos = unflattened + ++ if (len(flattend_key) > 1 ++ and not flattend_key[0] in convert_to_list ++ and not flattend_key[0] in unflattened): ++ convert_to_list.append(flattend_key[0]) ++ + for key in flattend_key[:-1]: + try: + current_pos = current_pos[key] +- except IndexError: +- new_pos = {} +- current_pos.append(new_pos) +- current_pos = new_pos + except KeyError: +- new_pos = [] ++ new_pos = {} + current_pos[key] = new_pos + current_pos = new_pos + current_pos[flattend_key[-1]] = data[flattend_key] + ++ for key in convert_to_list: ++ unflattened[key] = [unflattened[key][s] ++ for s in sorted(unflattened[key])] ++ + return unflattened + + From 697db306609ad4286168da30ca521297a7be05f0 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 16 Oct 2020 11:59:05 +0200 Subject: [PATCH 067/213] Set ckan extensions default level to INFO --- images/ckan/2.7/Dockerfile | 2 ++ images/ckan/2.8/Dockerfile | 2 ++ images/ckan/2.9/Dockerfile | 2 ++ 3 files changed, 6 insertions(+) diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index dd56eb4..f33aa94 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -153,6 +153,8 @@ RUN pip install -e /srv/app/src/ckan && \ # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + # Set the default level for extensions to INFO + paster --plugin=ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ # Create the data directory mkdir ${DATA_DIR} && \ # Change ownership to app user diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 3815cae..3688b5b 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -147,6 +147,8 @@ RUN pip install -e /srv/app/src/ckan && \ # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + # Set the default level for extensions to INFO + paster --plugin=ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ # Create the data directory mkdir ${DATA_DIR} && \ # Change ownership to app user diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index ba0487a..0ea323c 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -163,6 +163,8 @@ RUN pip install -e /srv/app/src/ckan && \ mkdir ${DATA_DIR} && \ # Webassets can't be loaded from env variables at runtime, it needs to be in the config so that it is created ckan config-tool ${APP_DIR}/production.ini "ckan.webassets.path = ${DATA_DIR}/webassets" && \ + # Set the default level for extensions to INFO + ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ # Change ownership to app user chown -R ckan:ckan /srv/app From d6ecc8aa72e5c5dc08534f5a1f390c0573e0b273 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 21 Oct 2020 15:08:57 +0200 Subject: [PATCH 068/213] New CKAN releases 2.7.9 2.8.6 2.9.1 --- Readme.md | 8 ++--- compose/.env | 2 +- examples/harvest/.env | 2 +- examples/harvest/Dockerfile | 4 +-- examples/s3filestore/.env | 2 +- examples/s3filestore/Dockerfile | 4 +-- images/ckan/2.7/Dockerfile | 2 +- .../patches/.keep => 2.7/patches/.gitkeep} | 0 .../00_datapusher_fix_callback_url.patch | 18 ----------- .../01_dictization_functions_unflatten.patch | 32 ------------------- images/ckan/2.8/Dockerfile | 2 +- images/ckan/2.8/patches/.gitkeep | 0 .../00_datapusher_fix_callback_url.patch | 18 ----------- .../01_dictization_functions_unflatten.patch | 32 ------------------- images/ckan/2.9/Dockerfile | 2 +- images/ckan/2.9/patches/.gitkeep | 0 .../01_dictization_functions_unflatten.patch | 32 ------------------- 17 files changed, 14 insertions(+), 146 deletions(-) rename images/ckan/{2.9/patches/.keep => 2.7/patches/.gitkeep} (100%) delete mode 100644 images/ckan/2.7/patches/00_datapusher_fix_callback_url.patch delete mode 100644 images/ckan/2.7/patches/01_dictization_functions_unflatten.patch create mode 100644 images/ckan/2.8/patches/.gitkeep delete mode 100644 images/ckan/2.8/patches/00_datapusher_fix_callback_url.patch delete mode 100644 images/ckan/2.8/patches/01_dictization_functions_unflatten.patch create mode 100644 images/ckan/2.9/patches/.gitkeep delete mode 100644 images/ckan/2.9/patches/01_dictization_functions_unflatten.patch diff --git a/Readme.md b/Readme.md index 1cea446..ff95437 100644 --- a/Readme.md +++ b/Readme.md @@ -40,7 +40,7 @@ We recommend to use a multi-stage approach to extend the docker images that we p ################### ### Extensions #### ################### -FROM keitaro/ckan:2.9.0 as extbuild +FROM keitaro/ckan:2.9.1 as extbuild # Switch to the root user USER root @@ -54,7 +54,7 @@ RUN pip wheel --wheel-dir=/wheels git+https://github.com/acmecorp/ckanext-acme@0 ############ ### MAIN ### ############ -FROM keitaro/ckan:2.9.0 +FROM keitaro/ckan:2.9.1 # Add the custom extensions to the plugins list ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher acme @@ -84,9 +84,9 @@ You can add scripts to CKAN custom images and copy them to the *docker-afterinit ## Build To build a CKAN image run: ```sh -docker build --tag keitaro/ckan:2.9.0 images/ckan/2.9 +docker build --tag keitaro/ckan:2.9.1 images/ckan/2.9 ``` -The –-tag keitaro/ckan:2.9.0 flag sets the image name to ketiaro/ckan:2.9.0 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. +The –-tag keitaro/ckan:2.9.1 flag sets the image name to ketiaro/ckan:2.9.1 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. ## Upload to DockerHub >*It's recommended to upload built images to DockerHub* diff --git a/compose/.env b/compose/.env index a8a815e..514724c 100644 --- a/compose/.env +++ b/compose/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.0 +CKAN_VERSION=2.9.1 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/harvest/.env b/examples/harvest/.env index a8a815e..514724c 100644 --- a/examples/harvest/.env +++ b/examples/harvest/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.0 +CKAN_VERSION=2.9.1 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index 47aaa55..098508b 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM keitaro/ckan:2.9.0 as extbuild +FROM keitaro/ckan:2.9.1 as extbuild MAINTAINER Keitaro Inc @@ -28,7 +28,7 @@ RUN curl -o /wheels/harvest.txt https://raw.githubusercontent.com/ckan/ckanext-h ############ ### MAIN ### ############ -FROM keitaro/ckan:2.9.0 +FROM keitaro/ckan:2.9.1 ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher harvest ckan_harvester diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env index 542a31a..d66972b 100644 --- a/examples/s3filestore/.env +++ b/examples/s3filestore/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.8.5 +CKAN_VERSION=2.8.6 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index 1d566cf..49994a2 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM keitaro/ckan:2.8.5 as extbuild +FROM keitaro/ckan:2.8.6 as extbuild MAINTAINER Keitaro Inc @@ -20,7 +20,7 @@ RUN curl -o /wheels/s3filestore.txt https://raw.githubusercontent.com/keitaroinc ############ ### MAIN ### ############ -FROM keitaro/ckan:2.8.5 +FROM keitaro/ckan:2.8.6 MAINTAINER Keitaro Inc diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index f33aa94..ec72633 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -5,7 +5,7 @@ FROM alpine:3.12 as ckanbuild # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.7.8 +ENV GIT_BRANCH=ckan-2.7.9 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.9/patches/.keep b/images/ckan/2.7/patches/.gitkeep similarity index 100% rename from images/ckan/2.9/patches/.keep rename to images/ckan/2.7/patches/.gitkeep diff --git a/images/ckan/2.7/patches/00_datapusher_fix_callback_url.patch b/images/ckan/2.7/patches/00_datapusher_fix_callback_url.patch deleted file mode 100644 index 83d03d5..0000000 --- a/images/ckan/2.7/patches/00_datapusher_fix_callback_url.patch +++ /dev/null @@ -1,18 +0,0 @@ ---- ckan/ckanext/datapusher/logic/action.py 2020-09-19 21:25:06.525350202 +0200 -+++ action_fix_callback_url.py 2020-09-19 21:27:43.596501963 +0200 -@@ -61,13 +61,13 @@ - - datapusher_url = config.get('ckan.datapusher.url') - -- site_url = h.url_for('/', qualified=True) -- - callback_url_base = config.get('ckan.datapusher.callback_url_base') - if callback_url_base: -+ site_url = callback_url_base - callback_url = urlparse.urljoin( - callback_url_base.rstrip('/'), '/api/3/action/datapusher_hook') - else: -+ site_url = h.url_for('/', qualified=True) - callback_url = h.url_for( - '/api/3/action/datapusher_hook', qualified=True) - diff --git a/images/ckan/2.7/patches/01_dictization_functions_unflatten.patch b/images/ckan/2.7/patches/01_dictization_functions_unflatten.patch deleted file mode 100644 index 51258cd..0000000 --- a/images/ckan/2.7/patches/01_dictization_functions_unflatten.patch +++ /dev/null @@ -1,32 +0,0 @@ ---- ckan/ckan/lib/navl/dictization_functions.py 2020-10-14 10:48:40.146291362 +0200 -+++ dictization_functions_unflatten_patch.py 2020-10-14 10:48:29.979771608 +0200 -@@ -440,19 +440,24 @@ - for flattend_key in sorted(data.keys(), key=flattened_order_key): - current_pos = unflattened - -+ if (len(flattend_key) > 1 -+ and not flattend_key[0] in convert_to_list -+ and not flattend_key[0] in unflattened): -+ convert_to_list.append(flattend_key[0]) -+ - for key in flattend_key[:-1]: - try: - current_pos = current_pos[key] -- except IndexError: -- new_pos = {} -- current_pos.append(new_pos) -- current_pos = new_pos - except KeyError: -- new_pos = [] -+ new_pos = {} - current_pos[key] = new_pos - current_pos = new_pos - current_pos[flattend_key[-1]] = data[flattend_key] - -+ for key in convert_to_list: -+ unflattened[key] = [unflattened[key][s] -+ for s in sorted(unflattened[key])] -+ - return unflattened - - diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 3688b5b..a7d9e24 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -5,7 +5,7 @@ FROM alpine:3.12 as ckanbuild # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.5 +ENV GIT_BRANCH=ckan-2.8.6 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.8/patches/.gitkeep b/images/ckan/2.8/patches/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/images/ckan/2.8/patches/00_datapusher_fix_callback_url.patch b/images/ckan/2.8/patches/00_datapusher_fix_callback_url.patch deleted file mode 100644 index 83d03d5..0000000 --- a/images/ckan/2.8/patches/00_datapusher_fix_callback_url.patch +++ /dev/null @@ -1,18 +0,0 @@ ---- ckan/ckanext/datapusher/logic/action.py 2020-09-19 21:25:06.525350202 +0200 -+++ action_fix_callback_url.py 2020-09-19 21:27:43.596501963 +0200 -@@ -61,13 +61,13 @@ - - datapusher_url = config.get('ckan.datapusher.url') - -- site_url = h.url_for('/', qualified=True) -- - callback_url_base = config.get('ckan.datapusher.callback_url_base') - if callback_url_base: -+ site_url = callback_url_base - callback_url = urlparse.urljoin( - callback_url_base.rstrip('/'), '/api/3/action/datapusher_hook') - else: -+ site_url = h.url_for('/', qualified=True) - callback_url = h.url_for( - '/api/3/action/datapusher_hook', qualified=True) - diff --git a/images/ckan/2.8/patches/01_dictization_functions_unflatten.patch b/images/ckan/2.8/patches/01_dictization_functions_unflatten.patch deleted file mode 100644 index 573cffd..0000000 --- a/images/ckan/2.8/patches/01_dictization_functions_unflatten.patch +++ /dev/null @@ -1,32 +0,0 @@ ---- ckan/ckan/lib/navl/dictization_functions.py 2020-10-14 10:39:46.319132692 +0200 -+++ dictization_functions_unflatten_patch.py 2020-10-14 10:40:38.204657981 +0200 -@@ -442,19 +442,24 @@ - for flattend_key in sorted(data.keys(), key=flattened_order_key): - current_pos = unflattened - -+ if (len(flattend_key) > 1 -+ and not flattend_key[0] in convert_to_list -+ and not flattend_key[0] in unflattened): -+ convert_to_list.append(flattend_key[0]) -+ - for key in flattend_key[:-1]: - try: - current_pos = current_pos[key] -- except IndexError: -- new_pos = {} -- current_pos.append(new_pos) -- current_pos = new_pos - except KeyError: -- new_pos = [] -+ new_pos = {} - current_pos[key] = new_pos - current_pos = new_pos - current_pos[flattend_key[-1]] = data[flattend_key] - -+ for key in convert_to_list: -+ unflattened[key] = [unflattened[key][s] -+ for s in sorted(unflattened[key])] -+ - return unflattened - - diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 0ea323c..de69247 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -5,7 +5,7 @@ FROM alpine:3.12 as ckanbuild # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.0 +ENV GIT_BRANCH=ckan-2.9.1 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.9/patches/.gitkeep b/images/ckan/2.9/patches/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/images/ckan/2.9/patches/01_dictization_functions_unflatten.patch b/images/ckan/2.9/patches/01_dictization_functions_unflatten.patch deleted file mode 100644 index c4acfc3..0000000 --- a/images/ckan/2.9/patches/01_dictization_functions_unflatten.patch +++ /dev/null @@ -1,32 +0,0 @@ ---- ckan/ckan/lib/navl/dictization_functions.py 2020-10-14 10:45:07.519582188 +0200 -+++ dictization_functions_unflatten_patch.py 2020-10-14 10:46:26.258305671 +0200 -@@ -410,19 +410,24 @@ - for flattend_key in sorted(data.keys(), key=flattened_order_key): - current_pos = unflattened - -+ if (len(flattend_key) > 1 -+ and not flattend_key[0] in convert_to_list -+ and not flattend_key[0] in unflattened): -+ convert_to_list.append(flattend_key[0]) -+ - for key in flattend_key[:-1]: - try: - current_pos = current_pos[key] -- except IndexError: -- new_pos = {} -- current_pos.append(new_pos) -- current_pos = new_pos - except KeyError: -- new_pos = [] -+ new_pos = {} - current_pos[key] = new_pos - current_pos = new_pos - current_pos[flattend_key[-1]] = data[flattend_key] - -+ for key in convert_to_list: -+ unflattened[key] = [unflattened[key][s] -+ for s in sorted(unflattened[key])] -+ - return unflattened - - From 1cfba750272c54d8657018df5c76d9406d0dd068 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Thu, 22 Oct 2020 18:11:23 +0200 Subject: [PATCH 069/213] Add setup for maintenance mode for CKAN --- compose/.ckan-env | 3 +++ compose/solr/solrconfig-2.7.9.xml | 1 + ...rconfig-2.8.5.xml => solrconfig-2.8.6.xml} | 0 ...rconfig-2.9.0.xml => solrconfig-2.9.1.xml} | 0 .../api/3/action/status_show/index.html | 9 ++++++++ .../ckan/2.7/setup/app/maintenance/index.html | 9 ++++++++ .../ckan/2.7/setup/app/maintenance/serve.py | 22 +++++++++++++++++++ images/ckan/2.7/setup/app/start_ckan.sh | 3 +++ .../api/3/action/status_show/index.html | 9 ++++++++ .../ckan/2.8/setup/app/maintenance/index.html | 9 ++++++++ .../ckan/2.8/setup/app/maintenance/serve.py | 22 +++++++++++++++++++ images/ckan/2.8/setup/app/start_ckan.sh | 3 +++ .../api/3/action/status_show/index.html | 9 ++++++++ .../ckan/2.9/setup/app/maintenance/index.html | 9 ++++++++ .../ckan/2.9/setup/app/maintenance/serve.py | 19 ++++++++++++++++ images/ckan/2.9/setup/app/start_ckan.sh | 3 +++ 16 files changed, 130 insertions(+) create mode 120000 compose/solr/solrconfig-2.7.9.xml rename compose/solr/{solrconfig-2.8.5.xml => solrconfig-2.8.6.xml} (100%) rename compose/solr/{solrconfig-2.9.0.xml => solrconfig-2.9.1.xml} (100%) create mode 100644 images/ckan/2.7/setup/app/maintenance/api/3/action/status_show/index.html create mode 100644 images/ckan/2.7/setup/app/maintenance/index.html create mode 100644 images/ckan/2.7/setup/app/maintenance/serve.py create mode 100644 images/ckan/2.8/setup/app/maintenance/api/3/action/status_show/index.html create mode 100644 images/ckan/2.8/setup/app/maintenance/index.html create mode 100644 images/ckan/2.8/setup/app/maintenance/serve.py create mode 100644 images/ckan/2.9/setup/app/maintenance/api/3/action/status_show/index.html create mode 100644 images/ckan/2.9/setup/app/maintenance/index.html create mode 100644 images/ckan/2.9/setup/app/maintenance/serve.py diff --git a/compose/.ckan-env b/compose/.ckan-env index 59e99bd..f67c88b 100644 --- a/compose/.ckan-env +++ b/compose/.ckan-env @@ -2,6 +2,9 @@ # Information about how it works: https://github.com/okfn/ckanext-envvars # Note that variables here take presedence over build/up time variables in .env +# Set to true to disable CKAN from starting and serve a maintenance page +MAINTENANCE_MODE=false + # General Settings CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 diff --git a/compose/solr/solrconfig-2.7.9.xml b/compose/solr/solrconfig-2.7.9.xml new file mode 120000 index 0000000..9859b31 --- /dev/null +++ b/compose/solr/solrconfig-2.7.9.xml @@ -0,0 +1 @@ +solrconfig-2.8.6.xml \ No newline at end of file diff --git a/compose/solr/solrconfig-2.8.5.xml b/compose/solr/solrconfig-2.8.6.xml similarity index 100% rename from compose/solr/solrconfig-2.8.5.xml rename to compose/solr/solrconfig-2.8.6.xml diff --git a/compose/solr/solrconfig-2.9.0.xml b/compose/solr/solrconfig-2.9.1.xml similarity index 100% rename from compose/solr/solrconfig-2.9.0.xml rename to compose/solr/solrconfig-2.9.1.xml diff --git a/images/ckan/2.7/setup/app/maintenance/api/3/action/status_show/index.html b/images/ckan/2.7/setup/app/maintenance/api/3/action/status_show/index.html new file mode 100644 index 0000000..eb43256 --- /dev/null +++ b/images/ckan/2.7/setup/app/maintenance/api/3/action/status_show/index.html @@ -0,0 +1,9 @@ + + + Maintenance + + +

Maintenance

+

Our data portal is currently in maintenance, please try in a while.

+ + diff --git a/images/ckan/2.7/setup/app/maintenance/index.html b/images/ckan/2.7/setup/app/maintenance/index.html new file mode 100644 index 0000000..eb43256 --- /dev/null +++ b/images/ckan/2.7/setup/app/maintenance/index.html @@ -0,0 +1,9 @@ + + + Maintenance + + +

Maintenance

+

Our data portal is currently in maintenance, please try in a while.

+ + diff --git a/images/ckan/2.7/setup/app/maintenance/serve.py b/images/ckan/2.7/setup/app/maintenance/serve.py new file mode 100644 index 0000000..23e2dde --- /dev/null +++ b/images/ckan/2.7/setup/app/maintenance/serve.py @@ -0,0 +1,22 @@ +from SimpleHTTPServer import SimpleHTTPRequestHandler +from BaseHTTPServer import HTTPServer +from SocketServer import ThreadingMixIn +import os + +PORT = 5000 + +web_dir = os.path.join(os.path.dirname(__file__)) +os.chdir(web_dir) + +Handler = SimpleHTTPRequestHandler + + +class MaintenanceServer(ThreadingMixIn, HTTPServer): + """Handle requests in a separate thread.""" + + +if __name__ == "__main__": + httpd = MaintenanceServer(("0.0.0.0", PORT), Handler) + print("Starting maintenance mode") + httpd.serve_forever() + diff --git a/images/ckan/2.7/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh index 722783a..95b4ad5 100755 --- a/images/ckan/2.7/setup/app/start_ckan.sh +++ b/images/ckan/2.7/setup/app/start_ckan.sh @@ -18,6 +18,9 @@ UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --e # Run the prerun script to init CKAN and create the default admin user python prerun.py +# Check if we are in maintenance mode and if yes serve the maintenance pages +if [ "$MAINTENANCE_MODE" = true ]; then PYTHONUNBUFFERED=1 python maintenance/serve.py; fi + # Run any after prerun/init scripts provided by images extending this one if [[ -d "${APP_DIR}/docker-afterinit.d" ]] then diff --git a/images/ckan/2.8/setup/app/maintenance/api/3/action/status_show/index.html b/images/ckan/2.8/setup/app/maintenance/api/3/action/status_show/index.html new file mode 100644 index 0000000..eb43256 --- /dev/null +++ b/images/ckan/2.8/setup/app/maintenance/api/3/action/status_show/index.html @@ -0,0 +1,9 @@ + + + Maintenance + + +

Maintenance

+

Our data portal is currently in maintenance, please try in a while.

+ + diff --git a/images/ckan/2.8/setup/app/maintenance/index.html b/images/ckan/2.8/setup/app/maintenance/index.html new file mode 100644 index 0000000..eb43256 --- /dev/null +++ b/images/ckan/2.8/setup/app/maintenance/index.html @@ -0,0 +1,9 @@ + + + Maintenance + + +

Maintenance

+

Our data portal is currently in maintenance, please try in a while.

+ + diff --git a/images/ckan/2.8/setup/app/maintenance/serve.py b/images/ckan/2.8/setup/app/maintenance/serve.py new file mode 100644 index 0000000..23e2dde --- /dev/null +++ b/images/ckan/2.8/setup/app/maintenance/serve.py @@ -0,0 +1,22 @@ +from SimpleHTTPServer import SimpleHTTPRequestHandler +from BaseHTTPServer import HTTPServer +from SocketServer import ThreadingMixIn +import os + +PORT = 5000 + +web_dir = os.path.join(os.path.dirname(__file__)) +os.chdir(web_dir) + +Handler = SimpleHTTPRequestHandler + + +class MaintenanceServer(ThreadingMixIn, HTTPServer): + """Handle requests in a separate thread.""" + + +if __name__ == "__main__": + httpd = MaintenanceServer(("0.0.0.0", PORT), Handler) + print("Starting maintenance mode") + httpd.serve_forever() + diff --git a/images/ckan/2.8/setup/app/start_ckan.sh b/images/ckan/2.8/setup/app/start_ckan.sh index 722783a..95b4ad5 100755 --- a/images/ckan/2.8/setup/app/start_ckan.sh +++ b/images/ckan/2.8/setup/app/start_ckan.sh @@ -18,6 +18,9 @@ UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --e # Run the prerun script to init CKAN and create the default admin user python prerun.py +# Check if we are in maintenance mode and if yes serve the maintenance pages +if [ "$MAINTENANCE_MODE" = true ]; then PYTHONUNBUFFERED=1 python maintenance/serve.py; fi + # Run any after prerun/init scripts provided by images extending this one if [[ -d "${APP_DIR}/docker-afterinit.d" ]] then diff --git a/images/ckan/2.9/setup/app/maintenance/api/3/action/status_show/index.html b/images/ckan/2.9/setup/app/maintenance/api/3/action/status_show/index.html new file mode 100644 index 0000000..eb43256 --- /dev/null +++ b/images/ckan/2.9/setup/app/maintenance/api/3/action/status_show/index.html @@ -0,0 +1,9 @@ + + + Maintenance + + +

Maintenance

+

Our data portal is currently in maintenance, please try in a while.

+ + diff --git a/images/ckan/2.9/setup/app/maintenance/index.html b/images/ckan/2.9/setup/app/maintenance/index.html new file mode 100644 index 0000000..eb43256 --- /dev/null +++ b/images/ckan/2.9/setup/app/maintenance/index.html @@ -0,0 +1,9 @@ + + + Maintenance + + +

Maintenance

+

Our data portal is currently in maintenance, please try in a while.

+ + diff --git a/images/ckan/2.9/setup/app/maintenance/serve.py b/images/ckan/2.9/setup/app/maintenance/serve.py new file mode 100644 index 0000000..fa56d40 --- /dev/null +++ b/images/ckan/2.9/setup/app/maintenance/serve.py @@ -0,0 +1,19 @@ +from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler +import os + +PORT = 5000 + +web_dir = os.path.join(os.path.dirname(__file__)) +os.chdir(web_dir) + + +def run(server_class=ThreadingHTTPServer, handler_class=SimpleHTTPRequestHandler): + server_address = ("0.0.0.0", PORT) + httpd = server_class(server_address, handler_class) + print("Starting maintenance mode") + httpd.serve_forever() + + +if __name__ == "__main__": + run() + diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index fee6687..3dc0046 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -18,6 +18,9 @@ UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid ckan --gid ckan --http :5000 --master # Run the prerun script to init CKAN and create the default admin user python prerun.py +# Check if we are in maintenance mode and if yes serve the maintenance pages +if [ "$MAINTENANCE_MODE" = true ]; then PYTHONUNBUFFERED=1 python maintenance/serve.py; fi + # Run any after prerun/init scripts provided by images extending this one if [[ -d "${APP_DIR}/docker-afterinit.d" ]] then From f7d1fedd155686724f2fc546551d2ba7a09d8f0d Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 2 Dec 2020 12:49:06 +0100 Subject: [PATCH 070/213] Add Ubuntu Focal docker images --- images/ckan/2.7/Dockerfile.focal | 225 ++++++++++++++++++++++++++++++ images/ckan/2.8/Dockerfile.focal | 214 +++++++++++++++++++++++++++++ images/ckan/2.9/Dockerfile.focal | 229 +++++++++++++++++++++++++++++++ 3 files changed, 668 insertions(+) create mode 100644 images/ckan/2.7/Dockerfile.focal create mode 100644 images/ckan/2.8/Dockerfile.focal create mode 100644 images/ckan/2.9/Dockerfile.focal diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal new file mode 100644 index 0000000..8ac2824 --- /dev/null +++ b/images/ckan/2.7/Dockerfile.focal @@ -0,0 +1,225 @@ +################## +### Build CKAN ### +################## +FROM ubuntu:focal-20201106 as ckanbuild + +# Set CKAN version to build +ENV GIT_URL=https://github.com/ckan/ckan.git +ENV GIT_BRANCH=ckan-2.7.9 + +# Set timezone +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Set Locale +ENV LC_ALL=en_US.UTF-8 + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +WORKDIR ${SRC_DIR} + +# Set the locale +RUN apt-get update +RUN apt-get install -y locales +RUN sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen +RUN dpkg-reconfigure --frontend=noninteractive locales +RUN update-locale LANG=${LC_ALL} + +# Instal apt-utils +RUN apt-get install -y \ + apt-utils + +# Packages to build CKAN requirements and plugins +RUN apt-get install -y \ + git \ + wget \ + gnupg \ + curl \ + python \ + linux-headers-generic \ + gcc-10 \ + make \ + g++-10 \ + autoconf \ + automake \ + libtool \ + patch \ + libpcre3-dev \ + libpcre3 \ + python-dev \ + libffi-dev \ + libxml2-dev \ + libxslt-dev + +# Install version 9.x of postgresql-dev so that CKAN requirements can be built +RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - +RUN sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ focal-pgdg main" >> /etc/apt/sources.list.d/postgresql.list' +RUN apt-get update +RUN apt-get install -y \ + postgresql-server-dev-9.6 + +# Use gcc 10 +RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10 --slave /usr/bin/x86_64-linux-gnu-gcc x86_64-linux-gnu-gcc /usr/bin/x86_64-linux-gnu-gcc-10 + +#postgresql-server-dev-9.6 Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Fetch and build CKAN and requirements +RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan +# Copy patches and apply patches script +COPY ./patches ${SRC_DIR}/patches +COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh +# Apply patches +RUN ${SRC_DIR}/apply_ckan_patches.sh +RUN rm -rf /srv/app/src/ckan/.git +RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 + + +########################### +### Default-Extensions #### +########################### +FROM ubuntu:focal-20201106 as extbuild + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +# List of default extensions +ENV DEFAULT_EXTENSIONS envvars + +# Locations and tags, please use specific tags or revisions +ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars +ENV ENVVARS_GIT_BRANCH=0.0.1 + +RUN apt-get update && \ + apt-get install -y \ + git \ + curl \ + python \ + python-dev + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Fetch and build the default CKAN extensions +RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars + +############ +### MAIN ### +############ +FROM ubuntu:focal-20201106 + +MAINTAINER Keitaro Inc + +# Set Locale +ENV LC_ALL=en_US.UTF-8 + +# Set the locale +RUN apt-get update && \ + apt-get install -y locales && \ + sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen && \ + dpkg-reconfigure --frontend=noninteractive locales && \ + update-locale LANG=${LC_ALL} && \ + rm -rf /var/lib/apt/lists/* + +ENV APP_DIR=/srv/app +ENV SRC_DIR=/srv/app/src +ENV DATA_DIR=/srv/app/data +ENV PIP_SRC=${SRC_DIR} +ENV CKAN_SITE_URL=http://localhost:5000 +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher + +WORKDIR ${APP_DIR} + +# Install necessary packages to run CKAN +RUN apt-get update && \ + apt-get install -y \ + gettext \ + curl \ + wget \ + gnupg \ + python \ + libpython2.7 \ + libmagic1 \ + libpcre3 \ + libxslt1.1 \ + libxml2 \ + tzdata \ + apache2-utils && \ + rm -rf /var/lib/apt/lists/* && \ + # Install version 9.x of postgresql-client so that CKAN can run + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ + sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ focal-pgdg main" >> /etc/apt/sources.list.d/postgresql.list' && \ + apt-get update && \ + apt-get install -y \ + postgresql-client-9.6 && \ + rm -rf /var/lib/apt/lists/* && \ + # Create SRC_DIR + mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Get artifacts from build stages +COPY --from=ckanbuild /wheels /srv/app/wheels +COPY --from=extbuild /wheels /srv/app/ext_wheels +COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan + +# Additional install steps for build stages artifacts +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent + +# Create a local user and group to run the app +RUN groupadd -g 92 ckan && \ + useradd -rm -d /srv/app -s /bin/bash -g ckan -u 92 ckan + +# Install CKAN +RUN pip install -e /srv/app/src/ckan && \ + cd ${SRC_DIR}/ckan && \ + cp who.ini ${APP_DIR} && \ + pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ + # Install default CKAN extensions + pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars && \ + # Create and update CKAN config + # Set timezone + echo "UTC" > /etc/timezone && \ + # Generate CKAN config + paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ + paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + # Set the default level for extensions to INFO + paster --plugin=ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ + # Create the data directory + mkdir ${DATA_DIR} && \ + # Change ownership to app user + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/wheels /srv/app/ext_wheels + +# Copy necessary scripts +COPY setup/app ${APP_DIR} + +# Create entrypoint directory for children image scripts +ONBUILD RUN mkdir docker-entrypoint.d + +# Create afterinit directory for children image scripts +ONBUILD RUN mkdir docker-afterinit.d + +EXPOSE 5000 + +HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 + +USER ckan + +CMD ["/srv/app/start_ckan.sh"] diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal new file mode 100644 index 0000000..1fbbe09 --- /dev/null +++ b/images/ckan/2.8/Dockerfile.focal @@ -0,0 +1,214 @@ +################## +### Build CKAN ### +################## +FROM ubuntu:focal-20201106 as ckanbuild + +# Set CKAN version to build +ENV GIT_URL=https://github.com/ckan/ckan.git +ENV GIT_BRANCH=ckan-2.8.6 + +# Set timezone +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Set Locale +ENV LC_ALL=en_US.UTF-8 + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +WORKDIR ${SRC_DIR} + +# Set the locale +RUN apt-get update +RUN apt-get install -y locales +RUN sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen +RUN dpkg-reconfigure --frontend=noninteractive locales +RUN update-locale LANG=${LC_ALL} + +# Instal apt-utils +RUN apt-get install -y \ + apt-utils + +# Packages to build CKAN requirements and plugins +RUN apt-get install -y \ + git \ + curl \ + python \ + libpq-dev \ + linux-headers-generic \ + gcc-10 \ + make \ + g++-10 \ + autoconf \ + automake \ + libtool \ + patch \ + libpcre3-dev \ + libpcre3 \ + python-dev \ + libffi-dev \ + libxml2-dev \ + libxslt-dev + +# Use gcc 10 +RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10 --slave /usr/bin/x86_64-linux-gnu-gcc x86_64-linux-gnu-gcc /usr/bin/x86_64-linux-gnu-gcc-10 + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Fetch and build CKAN and requirements +RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan +# Copy patches and apply patches script +COPY ./patches ${SRC_DIR}/patches +COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh +# Apply patches +RUN ${SRC_DIR}/apply_ckan_patches.sh +RUN rm -rf /srv/app/src/ckan/.git +RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 + + +########################### +### Default-Extensions #### +########################### +FROM ubuntu:focal-20201106 as extbuild + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +# List of default extensions +ENV DEFAULT_EXTENSIONS envvars + +# Locations and tags, please use specific tags or revisions +ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars +ENV ENVVARS_GIT_BRANCH=0.0.1 + +RUN apt-get update && \ + apt-get install -y \ + git \ + curl \ + python \ + python-dev + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Fetch and build the default CKAN extensions +RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars + +############ +### MAIN ### +############ +FROM ubuntu:focal-20201106 + +MAINTAINER Keitaro Inc + +# Set timezone +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Set Locale +ENV LC_ALL=en_US.UTF-8 + +# Set the locale +RUN apt-get update && \ + apt-get install -y locales && \ + sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen && \ + dpkg-reconfigure --frontend=noninteractive locales && \ + update-locale LANG=${LC_ALL} && \ + rm -rf /var/lib/apt/lists/* + +ENV APP_DIR=/srv/app +ENV SRC_DIR=/srv/app/src +ENV DATA_DIR=/srv/app/data +ENV PIP_SRC=${SRC_DIR} +ENV CKAN_SITE_URL=http://localhost:5000 +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher + +WORKDIR ${APP_DIR} + +# Install necessary packages to run CKAN +RUN apt-get update && \ + apt-get install -y \ + gettext \ + curl \ + libpq5 \ + postgresql-client \ + python \ + libpython2.7 \ + libmagic1 \ + libpcre3 \ + libxslt1.1 \ + libxml2 \ + tzdata \ + apache2-utils && \ + rm -rf /var/lib/apt/lists/* && \ + # Create SRC_DIR + mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Get artifacts from build stages +COPY --from=ckanbuild /wheels /srv/app/wheels +COPY --from=extbuild /wheels /srv/app/ext_wheels +COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan + +# Additional install steps for build stages artifacts +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent + +# Create a local user and group to run the app +RUN groupadd -g 92 ckan && \ + useradd -rm -d /srv/app -s /bin/bash -g ckan -u 92 ckan + +# Install CKAN +RUN pip install -e /srv/app/src/ckan && \ + cd ${SRC_DIR}/ckan && \ + cp who.ini ${APP_DIR} && \ + pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ + # Install default CKAN extensions + pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars && \ + # Create and update CKAN config + # Set timezone + echo "UTC" > /etc/timezone && \ + # Generate CKAN config + paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ + paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + # Set the default level for extensions to INFO + paster --plugin=ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ + # Create the data directory + mkdir ${DATA_DIR} && \ + # Change ownership to app user + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/wheels /srv/app/ext_wheels + +# Copy necessary scripts +COPY setup/app ${APP_DIR} + +# Create entrypoint directory for children image scripts +ONBUILD RUN mkdir docker-entrypoint.d + +# Create afterinit directory for children image scripts +ONBUILD RUN mkdir docker-afterinit.d + +EXPOSE 5000 + +HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 + +USER ckan + +CMD ["/srv/app/start_ckan.sh"] diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal new file mode 100644 index 0000000..375c321 --- /dev/null +++ b/images/ckan/2.9/Dockerfile.focal @@ -0,0 +1,229 @@ +################## +### Build CKAN ### +################## +FROM ubuntu:focal-20201106 as ckanbuild + +# Set CKAN version to build +ENV GIT_URL=https://github.com/ckan/ckan.git +ENV GIT_BRANCH=ckan-2.9.1 + +# Set timezone +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Set Locale +ENV LC_ALL=en_US.UTF-8 + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +WORKDIR ${SRC_DIR} + +# Set the locale +RUN apt-get update +RUN apt-get install -y locales +RUN sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen +RUN dpkg-reconfigure --frontend=noninteractive locales +RUN update-locale LANG=${LC_ALL} + +# Instal apt-utils +RUN apt-get install -y \ + apt-utils + +# Packages to build CKAN requirements and plugins +RUN apt-get install -y \ + git \ + curl \ + python3 \ + libpq-dev \ + linux-headers-generic \ + gcc-10 \ + make \ + g++-10 \ + autoconf \ + automake \ + libtool \ + patch \ + libpcre3-dev \ + libpcre3 \ + python3-dev \ + libffi-dev \ + libxml2-dev \ + libxslt-dev + +# Use gcc 10 +RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10 --slave /usr/bin/x86_64-linux-gnu-gcc x86_64-linux-gnu-gcc /usr/bin/x86_64-linux-gnu-gcc-10 + +# Link python to python3 +RUN ln -s /usr/bin/python3 /usr/bin/python + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Downgrade setuptools so that CKAN requirements can be built +RUN pip install setuptools==44.1.0 + +# Fetch and build CKAN and requirements +RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan +# Copy patches and apply patches script +COPY ./patches ${SRC_DIR}/patches +COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh +# Apply patches +RUN ${SRC_DIR}/apply_ckan_patches.sh +RUN rm -rf /srv/app/src/ckan/.git +RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 + + +########################### +### Default-Extensions #### +########################### +FROM ubuntu:focal-20201106 as extbuild + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +# List of default extensions +ENV DEFAULT_EXTENSIONS envvars + +# Locations and tags, please use specific tags or revisions +ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars +ENV ENVVARS_GIT_BRANCH=0.0.1 + +RUN apt-get update && \ + apt-get install -y \ + git \ + curl \ + python3 \ + python3-dev + +# Link python to python3 +RUN ln -s /usr/bin/python3 /usr/bin/python + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Fetch and build the default CKAN extensions +RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars + +############ +### MAIN ### +############ +FROM ubuntu:focal-20201106 + +MAINTAINER Keitaro Inc + +# Set timezone +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Set Locale +ENV LC_ALL=en_US.UTF-8 + +# Set the locale +RUN apt-get update && \ + apt-get install -y locales && \ + sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen && \ + dpkg-reconfigure --frontend=noninteractive locales && \ + update-locale LANG=${LC_ALL} && \ + rm -rf /var/lib/apt/lists/* + +ENV APP_DIR=/srv/app +ENV SRC_DIR=/srv/app/src +ENV DATA_DIR=/srv/app/data +ENV PIP_SRC=${SRC_DIR} +ENV CKAN_SITE_URL=http://localhost:5000 +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher + +WORKDIR ${APP_DIR} + +# Install necessary packages to run CKAN +RUN apt-get update && \ + apt-get install -y \ + gettext \ + curl \ + libpq5 \ + postgresql-client \ + python3 \ + python3-distutils \ + libpython3.8 \ + libmagic1 \ + libpcre3 \ + libxslt1.1 \ + libxml2 \ + tzdata \ + apache2-utils && \ + rm -rf /var/lib/apt/lists/* && \ + # Create SRC_DIR + mkdir -p ${SRC_DIR} && \ + # Link python to python3 + ln -s /usr/bin/python3 /usr/bin/python + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Get artifacts from build stages +COPY --from=ckanbuild /wheels /srv/app/wheels +COPY --from=extbuild /wheels /srv/app/ext_wheels +COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan + +# Additional install steps for build stages artifacts +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent + +# Create a local user and group to run the app +RUN groupadd -g 92 ckan && \ + useradd -rm -d /srv/app -s /bin/bash -g ckan -u 92 ckan + +# Install CKAN +RUN pip install -e /srv/app/src/ckan && \ + cd ${SRC_DIR}/ckan && \ + cp who.ini ${APP_DIR} && \ + pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ + # Install default CKAN extensions + pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars && \ + # Create and update CKAN config + # Set timezone + echo "UTC" > /etc/timezone && \ + # Generate CKAN config + ckan generate config ${APP_DIR}/production.ini && \ + # Configure plugins + ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + # Create the data directory + mkdir ${DATA_DIR} && \ + # Webassets can't be loaded from env variables at runtime, it needs to be in the config so that it is created + ckan config-tool ${APP_DIR}/production.ini "ckan.webassets.path = ${DATA_DIR}/webassets" && \ + # Set the default level for extensions to INFO + ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ + # Change ownership to app user + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/wheels /srv/app/ext_wheels + +# Copy necessary scripts +COPY setup/app ${APP_DIR} + +# Create entrypoint directory for children image scripts +ONBUILD RUN mkdir docker-entrypoint.d + +# Create afterinit directory for children image scripts +ONBUILD RUN mkdir docker-afterinit.d + +EXPOSE 5000 + +HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 + +USER ckan + +CMD ["/srv/app/start_ckan.sh"] From 35b390499a12a620aedace4c7c58aa40f1134ec6 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Wed, 2 Dec 2020 15:06:37 +0100 Subject: [PATCH 071/213] * adds git to main images * fixes timezone issue on 2.7 * minor formatting changes --- images/ckan/2.7/Dockerfile.focal | 15 +++++++++------ images/ckan/2.8/Dockerfile.focal | 3 +-- images/ckan/2.9/Dockerfile.focal | 23 +++++++++++------------ 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index 8ac2824..b1a6500 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -34,8 +34,8 @@ RUN apt-get install -y \ # Packages to build CKAN requirements and plugins RUN apt-get install -y \ git \ - wget \ - gnupg \ + wget \ + gnupg \ curl \ python \ linux-headers-generic \ @@ -122,6 +122,10 @@ FROM ubuntu:focal-20201106 MAINTAINER Keitaro Inc +# Set timezone +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + # Set Locale ENV LC_ALL=en_US.UTF-8 @@ -147,8 +151,9 @@ RUN apt-get update && \ apt-get install -y \ gettext \ curl \ - wget \ - gnupg \ + git \ + wget \ + gnupg \ python \ libpython2.7 \ libmagic1 \ @@ -192,8 +197,6 @@ RUN pip install -e /srv/app/src/ckan && \ # Install default CKAN extensions pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars && \ # Create and update CKAN config - # Set timezone - echo "UTC" > /etc/timezone && \ # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index 1fbbe09..5fc56f2 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -143,6 +143,7 @@ RUN apt-get update && \ apt-get install -y \ gettext \ curl \ + git \ libpq5 \ postgresql-client \ python \ @@ -181,8 +182,6 @@ RUN pip install -e /srv/app/src/ckan && \ # Install default CKAN extensions pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars && \ # Create and update CKAN config - # Set timezone - echo "UTC" > /etc/timezone && \ # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 375c321..6d96f94 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -38,12 +38,12 @@ RUN apt-get install -y \ python3 \ libpq-dev \ linux-headers-generic \ - gcc-10 \ - make \ - g++-10 \ - autoconf \ - automake \ - libtool \ + gcc-10 \ + make \ + g++-10 \ + autoconf \ + automake \ + libtool \ patch \ libpcre3-dev \ libpcre3 \ @@ -151,12 +151,13 @@ WORKDIR ${APP_DIR} RUN apt-get update && \ apt-get install -y \ gettext \ - curl \ + curl \ libpq5 \ - postgresql-client \ + git \ + postgresql-client \ python3 \ - python3-distutils \ - libpython3.8 \ + python3-distutils \ + libpython3.8 \ libmagic1 \ libpcre3 \ libxslt1.1 \ @@ -193,8 +194,6 @@ RUN pip install -e /srv/app/src/ckan && \ # Install default CKAN extensions pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars && \ # Create and update CKAN config - # Set timezone - echo "UTC" > /etc/timezone && \ # Generate CKAN config ckan generate config ${APP_DIR}/production.ini && \ # Configure plugins From be0543b6c6ace3123749b40a029a513593ef430f Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Wed, 9 Dec 2020 16:34:21 +0100 Subject: [PATCH 072/213] fixes crash when limit is sent to the get action --- .../ckan/2.9/patches/00_get_limit_type_check.patch | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 images/ckan/2.9/patches/00_get_limit_type_check.patch diff --git a/images/ckan/2.9/patches/00_get_limit_type_check.patch b/images/ckan/2.9/patches/00_get_limit_type_check.patch new file mode 100644 index 0000000..7fff75b --- /dev/null +++ b/images/ckan/2.9/patches/00_get_limit_type_check.patch @@ -0,0 +1,13 @@ +--- ckan/ckan/logic/action/get.py 2020-12-09 12:05:49.800909223 +0100 ++++ get_limit_type_check.py 2020-12-09 12:11:58.404000000 +0100 +@@ -371,8 +371,8 @@ + 'ckan.group_and_organization_list_all_fields_max', 25) + else: + max_limit = config.get('ckan.group_and_organization_list_max', 1000) +- if limit is None or limit > max_limit: +- limit = max_limit ++ if limit is None or int(limit) > max_limit: ++ limit = string(max_limit) + + # order_by deprecated in ckan 1.8 + # if it is supplied and sort isn't use order_by and raise a warning From 87808189ecc918d19401f978363bcd8b15d16043 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Wed, 9 Dec 2020 18:17:20 +0100 Subject: [PATCH 073/213] fix for the get action limit patch --- images/ckan/2.9/patches/00_get_limit_type_check.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/ckan/2.9/patches/00_get_limit_type_check.patch b/images/ckan/2.9/patches/00_get_limit_type_check.patch index 7fff75b..6adc118 100644 --- a/images/ckan/2.9/patches/00_get_limit_type_check.patch +++ b/images/ckan/2.9/patches/00_get_limit_type_check.patch @@ -7,7 +7,7 @@ - if limit is None or limit > max_limit: - limit = max_limit + if limit is None or int(limit) > max_limit: -+ limit = string(max_limit) ++ limit = str(max_limit) # order_by deprecated in ckan 1.8 # if it is supplied and sort isn't use order_by and raise a warning From 2485af55b00029aa4b6a9af27bacd5c7ff54bbae Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 16 Dec 2020 20:22:35 +0100 Subject: [PATCH 074/213] Add a ckan alias command for 2.7 and 2.8 so it can be forward-compatible with 2.9 --- images/ckan/2.7/Dockerfile | 3 +++ images/ckan/2.7/setup/bin/ckan | 14 ++++++++++++++ images/ckan/2.8/Dockerfile | 3 +++ images/ckan/2.8/setup/bin/ckan | 14 ++++++++++++++ 4 files changed, 34 insertions(+) create mode 100755 images/ckan/2.7/setup/bin/ckan create mode 100755 images/ckan/2.8/setup/bin/ckan diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index ec72633..fecaf1e 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -166,6 +166,9 @@ RUN rm -rf /srv/app/wheels /srv/app/ext_wheels # Copy necessary scripts COPY setup/app ${APP_DIR} +# Copy the alias script for paster to be ckan so it's compatible with 2.9 +COPY setup/bin/ckan /usr/bin/ckan + # Create entrypoint directory for children image scripts ONBUILD RUN mkdir docker-entrypoint.d diff --git a/images/ckan/2.7/setup/bin/ckan b/images/ckan/2.7/setup/bin/ckan new file mode 100755 index 0000000..0389541 --- /dev/null +++ b/images/ckan/2.7/setup/bin/ckan @@ -0,0 +1,14 @@ +#!/bin/bash +# CKAN <2.9 uses paster and the ckan plugin for paster which expects the configuration to be passed +# to a subcommand, example: paster --plugin=ckan datastore -c /srv/app/production.ini set-permissions +# CKAN >2.9 ckan CLI command expects the configuration option as the second and third parameter +# Example: ckan -c /srv/app/production.ini datastore set-permissions +# +# Shift the two arguments so that we can move them to the end of the argument list +# ckan -c /srv/app/production.ini datastore set-permissions +# becomes +# paster --plugin datastore set-permissions -c /srv/app/production.ini +config_switch=$1 +config_file=$2 +shift 2 +paster --plugin=ckan "$@" "$config_switch" "$config_file" diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index a7d9e24..4ec6e6c 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -160,6 +160,9 @@ RUN rm -rf /srv/app/wheels /srv/app/ext_wheels # Copy necessary scripts COPY setup/app ${APP_DIR} +# Copy the alias script for paster to be ckan so it's compatible with 2.9 +COPY setup/bin/ckan /usr/bin/ckan + # Create entrypoint directory for children image scripts ONBUILD RUN mkdir docker-entrypoint.d diff --git a/images/ckan/2.8/setup/bin/ckan b/images/ckan/2.8/setup/bin/ckan new file mode 100755 index 0000000..0389541 --- /dev/null +++ b/images/ckan/2.8/setup/bin/ckan @@ -0,0 +1,14 @@ +#!/bin/bash +# CKAN <2.9 uses paster and the ckan plugin for paster which expects the configuration to be passed +# to a subcommand, example: paster --plugin=ckan datastore -c /srv/app/production.ini set-permissions +# CKAN >2.9 ckan CLI command expects the configuration option as the second and third parameter +# Example: ckan -c /srv/app/production.ini datastore set-permissions +# +# Shift the two arguments so that we can move them to the end of the argument list +# ckan -c /srv/app/production.ini datastore set-permissions +# becomes +# paster --plugin datastore set-permissions -c /srv/app/production.ini +config_switch=$1 +config_file=$2 +shift 2 +paster --plugin=ckan "$@" "$config_switch" "$config_file" From 7f974b661591aa6c6b0864bbbe3c22ebfe3b8d8c Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 16 Dec 2020 20:40:10 +0100 Subject: [PATCH 075/213] Add ckan command alias for focal images --- images/ckan/2.7/Dockerfile.focal | 3 +++ images/ckan/2.8/Dockerfile.focal | 3 +++ 2 files changed, 6 insertions(+) diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index b1a6500..5d69961 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -213,6 +213,9 @@ RUN rm -rf /srv/app/wheels /srv/app/ext_wheels # Copy necessary scripts COPY setup/app ${APP_DIR} +# Copy the alias script for paster to be ckan so it's compatible with 2.9 +COPY setup/bin/ckan /usr/bin/ckan + # Create entrypoint directory for children image scripts ONBUILD RUN mkdir docker-entrypoint.d diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index 5fc56f2..c539e08 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -198,6 +198,9 @@ RUN rm -rf /srv/app/wheels /srv/app/ext_wheels # Copy necessary scripts COPY setup/app ${APP_DIR} +# Copy the alias script for paster to be ckan so it's compatible with 2.9 +COPY setup/bin/ckan /usr/bin/ckan + # Create entrypoint directory for children image scripts ONBUILD RUN mkdir docker-entrypoint.d From 9247cb4acb843034742183123f39c88276de886b Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 16 Dec 2020 20:43:36 +0100 Subject: [PATCH 076/213] Reorder the argument list for the ckan alias command --- images/ckan/2.7/setup/bin/ckan | 9 +++++---- images/ckan/2.8/setup/bin/ckan | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/images/ckan/2.7/setup/bin/ckan b/images/ckan/2.7/setup/bin/ckan index 0389541..abf14e7 100755 --- a/images/ckan/2.7/setup/bin/ckan +++ b/images/ckan/2.7/setup/bin/ckan @@ -4,11 +4,12 @@ # CKAN >2.9 ckan CLI command expects the configuration option as the second and third parameter # Example: ckan -c /srv/app/production.ini datastore set-permissions # -# Shift the two arguments so that we can move them to the end of the argument list +# Shift three arguments so that we can reorder the argument list # ckan -c /srv/app/production.ini datastore set-permissions # becomes -# paster --plugin datastore set-permissions -c /srv/app/production.ini +# paster --plugin datastore -c /srv/app/production.ini set-permissions config_switch=$1 config_file=$2 -shift 2 -paster --plugin=ckan "$@" "$config_switch" "$config_file" +subcommand=$3 +shift 3 +paster --plugin=ckan "$subcommand" "$config_switch" "$config_file" "$@" diff --git a/images/ckan/2.8/setup/bin/ckan b/images/ckan/2.8/setup/bin/ckan index 0389541..abf14e7 100755 --- a/images/ckan/2.8/setup/bin/ckan +++ b/images/ckan/2.8/setup/bin/ckan @@ -4,11 +4,12 @@ # CKAN >2.9 ckan CLI command expects the configuration option as the second and third parameter # Example: ckan -c /srv/app/production.ini datastore set-permissions # -# Shift the two arguments so that we can move them to the end of the argument list +# Shift three arguments so that we can reorder the argument list # ckan -c /srv/app/production.ini datastore set-permissions # becomes -# paster --plugin datastore set-permissions -c /srv/app/production.ini +# paster --plugin datastore -c /srv/app/production.ini set-permissions config_switch=$1 config_file=$2 -shift 2 -paster --plugin=ckan "$@" "$config_switch" "$config_file" +subcommand=$3 +shift 3 +paster --plugin=ckan "$subcommand" "$config_switch" "$config_file" "$@" From b593510f53364bf55be68d02189d6341b4805f6c Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 16 Dec 2020 20:48:57 +0100 Subject: [PATCH 077/213] mend Reorder the argument list for the ckan alias command --- images/ckan/2.7/setup/bin/ckan | 2 +- images/ckan/2.8/setup/bin/ckan | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/images/ckan/2.7/setup/bin/ckan b/images/ckan/2.7/setup/bin/ckan index abf14e7..f2e03dd 100755 --- a/images/ckan/2.7/setup/bin/ckan +++ b/images/ckan/2.7/setup/bin/ckan @@ -7,7 +7,7 @@ # Shift three arguments so that we can reorder the argument list # ckan -c /srv/app/production.ini datastore set-permissions # becomes -# paster --plugin datastore -c /srv/app/production.ini set-permissions +# paster --plugin=ckan datastore -c /srv/app/production.ini set-permissions config_switch=$1 config_file=$2 subcommand=$3 diff --git a/images/ckan/2.8/setup/bin/ckan b/images/ckan/2.8/setup/bin/ckan index abf14e7..f2e03dd 100755 --- a/images/ckan/2.8/setup/bin/ckan +++ b/images/ckan/2.8/setup/bin/ckan @@ -7,7 +7,7 @@ # Shift three arguments so that we can reorder the argument list # ckan -c /srv/app/production.ini datastore set-permissions # becomes -# paster --plugin datastore -c /srv/app/production.ini set-permissions +# paster --plugin=ckan datastore -c /srv/app/production.ini set-permissions config_switch=$1 config_file=$2 subcommand=$3 From 7aaa02a87e657bfa5f8b575977c9ea3bd9e19b1b Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Tue, 22 Dec 2020 00:15:16 +0100 Subject: [PATCH 078/213] Exit CKAN startup if prerun exits --- images/ckan/2.7/setup/app/start_ckan.sh | 2 +- images/ckan/2.8/setup/app/start_ckan.sh | 2 +- images/ckan/2.9/setup/app/start_ckan.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/images/ckan/2.7/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh index 95b4ad5..a0bc80d 100755 --- a/images/ckan/2.7/setup/app/start_ckan.sh +++ b/images/ckan/2.7/setup/app/start_ckan.sh @@ -16,7 +16,7 @@ fi UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --paste-logger /srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L --gevent-early-monkey-patch" # Run the prerun script to init CKAN and create the default admin user -python prerun.py +python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } # Check if we are in maintenance mode and if yes serve the maintenance pages if [ "$MAINTENANCE_MODE" = true ]; then PYTHONUNBUFFERED=1 python maintenance/serve.py; fi diff --git a/images/ckan/2.8/setup/app/start_ckan.sh b/images/ckan/2.8/setup/app/start_ckan.sh index 95b4ad5..a0bc80d 100755 --- a/images/ckan/2.8/setup/app/start_ckan.sh +++ b/images/ckan/2.8/setup/app/start_ckan.sh @@ -16,7 +16,7 @@ fi UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --paste-logger /srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L --gevent-early-monkey-patch" # Run the prerun script to init CKAN and create the default admin user -python prerun.py +python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } # Check if we are in maintenance mode and if yes serve the maintenance pages if [ "$MAINTENANCE_MODE" = true ]; then PYTHONUNBUFFERED=1 python maintenance/serve.py; fi diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index 3dc0046..fee6dad 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -16,7 +16,7 @@ fi UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid ckan --gid ckan --http :5000 --master --enable-threads --wsgi-file /srv/app/wsgi.py --module wsgi:application --lazy-apps --gevent 2000 -p 2 -L --gevent-early-monkey-patch --vacuum --harakiri 50 --callable application" # Run the prerun script to init CKAN and create the default admin user -python prerun.py +python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } # Check if we are in maintenance mode and if yes serve the maintenance pages if [ "$MAINTENANCE_MODE" = true ]; then PYTHONUNBUFFERED=1 python maintenance/serve.py; fi From fe1c05a350c5779346b72d8d55e83102f075e06c Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 15 Jan 2021 20:19:11 +0100 Subject: [PATCH 079/213] Uprade to alpine 3.13 --- images/ckan/2.7/Dockerfile | 6 +++--- images/ckan/2.8/Dockerfile | 6 +++--- images/ckan/2.9/Dockerfile | 6 +++--- images/datapusher/Dockerfile | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index fecaf1e..12bbd17 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.12 as ckanbuild +FROM alpine:3.13 as ckanbuild # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git @@ -61,7 +61,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.12 as extbuild +FROM alpine:3.13 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -93,7 +93,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.12 +FROM alpine:3.13 MAINTAINER Keitaro Inc diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 4ec6e6c..cf7a111 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.12 as ckanbuild +FROM alpine:3.13 as ckanbuild # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git @@ -57,7 +57,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.12 as extbuild +FROM alpine:3.13 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -89,7 +89,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.12 +FROM alpine:3.13 MAINTAINER Keitaro Inc diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index de69247..09ac216 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.12 as ckanbuild +FROM alpine:3.13 as ckanbuild # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git @@ -63,7 +63,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.12 as extbuild +FROM alpine:3.13 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -98,7 +98,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.12 +FROM alpine:3.13 MAINTAINER Keitaro Inc diff --git a/images/datapusher/Dockerfile b/images/datapusher/Dockerfile index d749297..89d1ed9 100644 --- a/images/datapusher/Dockerfile +++ b/images/datapusher/Dockerfile @@ -1,7 +1,7 @@ ############# ### Build ### ############# -FROM alpine:3.12 as build +FROM alpine:3.13 as build # Set datapusher version to build ENV GIT_URL https://github.com/keitaroinc/datapusher.git @@ -51,7 +51,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ############ ### MAIN ### ############ -FROM alpine:3.12 +FROM alpine:3.13 MAINTAINER Keitaro Inc From 8e92ed04db9ec73a52e789e8aaa3f6a1578e8400 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 15 Jan 2021 22:24:16 +0100 Subject: [PATCH 080/213] Lint Dockerfiles --- .hadolint.yaml | 3 +++ compose/postgresql/Dockerfile | 2 +- compose/solr/Dockerfile | 7 ++--- examples/harvest/Dockerfile | 8 +++--- examples/s3filestore/Dockerfile | 8 +++--- images/ckan/2.7/Dockerfile | 22 ++++++++------- images/ckan/2.7/Dockerfile.focal | 46 ++++++++++++++++++-------------- images/ckan/2.8/Dockerfile | 15 ++++++----- images/ckan/2.8/Dockerfile.focal | 30 ++++++++++++--------- images/ckan/2.9/Dockerfile | 15 ++++++----- images/ckan/2.9/Dockerfile.focal | 38 +++++++++++++++----------- images/datapusher/Dockerfile | 9 ++++--- 12 files changed, 119 insertions(+), 84 deletions(-) create mode 100644 .hadolint.yaml diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 0000000..502b578 --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,3 @@ +ignored: + - DL3018 + - DL3008 diff --git a/compose/postgresql/Dockerfile b/compose/postgresql/Dockerfile index f7e9795..d2dee7f 100644 --- a/compose/postgresql/Dockerfile +++ b/compose/postgresql/Dockerfile @@ -10,4 +10,4 @@ ARG POSTGRES_PASSWORD ARG DS_RO_PASS # Include datastore setup scripts -ADD ./postgresql/docker-entrypoint-initdb.d /docker-entrypoint-initdb.d +COPY ./postgresql/docker-entrypoint-initdb.d /docker-entrypoint-initdb.d diff --git a/compose/solr/Dockerfile b/compose/solr/Dockerfile index f49dd33..6cdbae2 100644 --- a/compose/solr/Dockerfile +++ b/compose/solr/Dockerfile @@ -12,8 +12,8 @@ RUN mkdir -p /opt/solr/server/solr/$SOLR_CORE/conf RUN mkdir -p /opt/solr/server/solr/$SOLR_CORE/data # Adding Files -ADD ./solr/solrconfig-$CKAN_VERSION.xml \ -https://raw.githubusercontent.com/ckan/ckan/ckan-$CKAN_VERSION/ckan/config/solr/schema.xml \ +COPY ./solr/solrconfig-$CKAN_VERSION.xml /opt/solr/server/solr/$SOLR_CORE/conf/ +ADD https://raw.githubusercontent.com/ckan/ckan/ckan-$CKAN_VERSION/ckan/config/solr/schema.xml \ https://raw.githubusercontent.com/apache/lucene-solr/releases/lucene-solr/$SOLR_VERSION/solr/server/solr/configsets/basic_configs/conf/currency.xml \ https://raw.githubusercontent.com/apache/lucene-solr/releases/lucene-solr/$SOLR_VERSION/solr/server/solr/configsets/basic_configs/conf/synonyms.txt \ https://raw.githubusercontent.com/apache/lucene-solr/releases/lucene-solr/$SOLR_VERSION/solr/server/solr/configsets/basic_configs/conf/stopwords.txt \ @@ -28,7 +28,8 @@ RUN mv /opt/solr/server/solr/$SOLR_CORE/conf/solrconfig-$CKAN_VERSION.xml /opt/s # Giving ownership to Solr USER root -RUN chown -R $SOLR_USER:$SOLR_USER /opt/solr/server/solr/$SOLR_CORE + +RUN chown -R "$SOLR_USER:$SOLR_USER" /opt/solr/server/solr/$SOLR_CORE # User USER $SOLR_USER:$SOLR_USER diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index 098508b..df3202f 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -3,8 +3,6 @@ ################### FROM keitaro/ckan:2.9.1 as extbuild -MAINTAINER Keitaro Inc - # Locations and tags, please use specific tags or revisions ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest ENV HARVEST_GIT_BRANCH=v1.3.1 @@ -25,11 +23,15 @@ RUN pip wheel --wheel-dir=/wheels git+${HARVEST_GIT_URL}@${HARVEST_GIT_BRANCH}#e RUN pip wheel --wheel-dir=/wheels -r https://raw.githubusercontent.com/ckan/ckanext-harvest/${HARVEST_GIT_BRANCH}/pip-requirements.txt RUN curl -o /wheels/harvest.txt https://raw.githubusercontent.com/ckan/ckanext-harvest/${HARVEST_GIT_BRANCH}/pip-requirements.txt +USER ckan + ############ ### MAIN ### ############ FROM keitaro/ckan:2.9.1 +LABEL maintainer="Keitaro Inc " + ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher harvest ckan_harvester # Switch to the root user @@ -41,7 +43,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels RUN pip install --no-index --find-links=/srv/app/ext_wheels ckanext-harvest && \ pip install --no-index --find-links=/srv/app/ext_wheels -r /srv/app/ext_wheels/harvest.txt && \ # Configure plugins - ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + ckan config-tool "${APP_DIR}/production.ini" "ckan.plugins = ${CKAN__PLUGINS}" && \ chown -R ckan:ckan /srv/app # Remove wheels diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index 49994a2..7e45e25 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -3,8 +3,6 @@ ################### FROM keitaro/ckan:2.8.6 as extbuild -MAINTAINER Keitaro Inc - # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore ENV S3FILESTORE_GIT_BRANCH=master @@ -17,12 +15,14 @@ RUN pip wheel --wheel-dir=/wheels git+${S3FILESTORE_GIT_URL}@${S3FILESTORE_GIT_B RUN pip wheel --wheel-dir=/wheels -r https://raw.githubusercontent.com/keitaroinc/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt RUN curl -o /wheels/s3filestore.txt https://raw.githubusercontent.com/keitaroinc/ckanext-s3filestore/${S3FILESTORE_GIT_BRANCH}/requirements.txt +USER ckan + ############ ### MAIN ### ############ FROM keitaro/ckan:2.8.6 -MAINTAINER Keitaro Inc +LABEL maintainer="Keitaro Inc " ENV CKAN__PLUGINS envvars s3filestore image_view text_view recline_view datastore datapusher @@ -34,7 +34,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels # Install and enable the custom extensions RUN pip install --no-index --find-links=/srv/app/ext_wheels ckanext-s3filestore && \ pip install --no-index --find-links=/srv/app/ext_wheels -r /srv/app/ext_wheels/s3filestore.txt && \ - paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + paster --plugin=ckan config-tool "${APP_DIR}/production.ini" "ckan.plugins = ${CKAN__PLUGINS}" && \ chown -R ckan:ckan /srv/app # Remove wheels diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 12bbd17..190fd4a 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -36,7 +36,7 @@ RUN apk add --no-cache \ libxslt-dev # Install version 9.x of postgresql-dev so that CKAN requirements can be built -RUN apk add --repository http://dl-cdn.alpinelinux.org/alpine/v3.6/main \ +RUN apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.6/main \ postgresql-dev~=9.6 # Create the src directory @@ -95,19 +95,20 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ FROM alpine:3.13 -MAINTAINER Keitaro Inc +LABEL maintainer="Keitaro Inc " +LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src +ENV CKAN_DIR=${SRC_DIR}/ckan ENV DATA_DIR=/srv/app/data ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher -WORKDIR ${APP_DIR} - # Install necessary packages to run CKAN -RUN apk add --no-cache git \ +RUN apk add --no-cache \ + git \ bash \ gettext \ curl \ @@ -119,7 +120,7 @@ RUN apk add --no-cache git \ tzdata \ apache2-utils && \ # Install version 9.x of postgresql-client so that CKAN can run - apk add --repository http://dl-cdn.alpinelinux.org/alpine/v3.6/main \ + apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.6/main \ postgresql-client~=9.6 && \ # Create SRC_DIR mkdir -p ${SRC_DIR} @@ -131,18 +132,19 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels COPY --from=extbuild /wheels /srv/app/ext_wheels -COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan +COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ adduser -u 92 -h /srv/app -H -D -S -G ckan ckan +WORKDIR ${CKAN_DIR} + # Install CKAN RUN pip install -e /srv/app/src/ckan && \ - cd ${SRC_DIR}/ckan && \ cp who.ini ${APP_DIR} && \ pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ # Install default CKAN extensions @@ -166,6 +168,8 @@ RUN rm -rf /srv/app/wheels /srv/app/ext_wheels # Copy necessary scripts COPY setup/app ${APP_DIR} +WORKDIR ${APP_DIR} + # Copy the alias script for paster to be ckan so it's compatible with 2.9 COPY setup/bin/ckan /usr/bin/ckan diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index 5d69961..8b2cb2d 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -22,21 +22,21 @@ WORKDIR ${SRC_DIR} # Set the locale RUN apt-get update -RUN apt-get install -y locales +RUN apt-get install --no-install-recommends -y locales RUN sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen RUN dpkg-reconfigure --frontend=noninteractive locales RUN update-locale LANG=${LC_ALL} # Instal apt-utils -RUN apt-get install -y \ +RUN apt-get install --no-install-recommends -y \ apt-utils # Packages to build CKAN requirements and plugins -RUN apt-get install -y \ +RUN apt-get install --no-install-recommends -y \ git \ - wget \ - gnupg \ + gnupg \ curl \ + ca-certificates \ python \ linux-headers-generic \ gcc-10 \ @@ -54,10 +54,11 @@ RUN apt-get install -y \ libxslt-dev # Install version 9.x of postgresql-dev so that CKAN requirements can be built -RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +RUN curl -s -o - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - RUN sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ focal-pgdg main" >> /etc/apt/sources.list.d/postgresql.list' RUN apt-get update -RUN apt-get install -y \ +RUN apt-get install --no-install-recommends -y \ postgresql-server-dev-9.6 # Use gcc 10 @@ -99,9 +100,10 @@ ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars ENV ENVVARS_GIT_BRANCH=0.0.1 RUN apt-get update && \ - apt-get install -y \ + apt-get install --no-install-recommends -y \ git \ curl \ + ca-certificates \ python \ python-dev @@ -120,7 +122,8 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ FROM ubuntu:focal-20201106 -MAINTAINER Keitaro Inc +LABEL maintainer="Keitaro Inc " +LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan # Set timezone ENV TZ=UTC @@ -131,7 +134,7 @@ ENV LC_ALL=en_US.UTF-8 # Set the locale RUN apt-get update && \ - apt-get install -y locales && \ + apt-get install --no-install-recommends -y locales && \ sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen && \ dpkg-reconfigure --frontend=noninteractive locales && \ update-locale LANG=${LC_ALL} && \ @@ -139,21 +142,21 @@ RUN apt-get update && \ ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src +ENV CKAN_DIR=${SRC_DIR}/ckan ENV DATA_DIR=/srv/app/data ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher -WORKDIR ${APP_DIR} - # Install necessary packages to run CKAN +SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN apt-get update && \ - apt-get install -y \ + apt-get install --no-install-recommends -y \ gettext \ curl \ + ca-certificates \ git \ - wget \ - gnupg \ + gnupg \ python \ libpython2.7 \ libmagic1 \ @@ -164,10 +167,10 @@ RUN apt-get update && \ apache2-utils && \ rm -rf /var/lib/apt/lists/* && \ # Install version 9.x of postgresql-client so that CKAN can run - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ + curl -s -o - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ focal-pgdg main" >> /etc/apt/sources.list.d/postgresql.list' && \ apt-get update && \ - apt-get install -y \ + apt-get install --no-install-recommends -y \ postgresql-client-9.6 && \ rm -rf /var/lib/apt/lists/* && \ # Create SRC_DIR @@ -180,18 +183,19 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels COPY --from=extbuild /wheels /srv/app/ext_wheels -COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan +COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 # Create a local user and group to run the app RUN groupadd -g 92 ckan && \ useradd -rm -d /srv/app -s /bin/bash -g ckan -u 92 ckan +WORKDIR ${CKAN_DIR} + # Install CKAN RUN pip install -e /srv/app/src/ckan && \ - cd ${SRC_DIR}/ckan && \ cp who.ini ${APP_DIR} && \ pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ # Install default CKAN extensions @@ -213,6 +217,8 @@ RUN rm -rf /srv/app/wheels /srv/app/ext_wheels # Copy necessary scripts COPY setup/app ${APP_DIR} +WORKDIR ${APP_DIR} + # Copy the alias script for paster to be ckan so it's compatible with 2.9 COPY setup/bin/ckan /usr/bin/ckan diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index cf7a111..c9abcbd 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -91,17 +91,17 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ FROM alpine:3.13 -MAINTAINER Keitaro Inc +LABEL maintainer="Keitaro Inc " +LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src +ENV CKAN_DIR=${SRC_DIR}/ckan ENV DATA_DIR=/srv/app/data ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher -WORKDIR ${APP_DIR} - # Install necessary packages to run CKAN RUN apk add --no-cache git \ bash \ @@ -125,18 +125,19 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels COPY --from=extbuild /wheels /srv/app/ext_wheels -COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan +COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ adduser -u 92 -h /srv/app -H -D -S -G ckan ckan +WORKDIR ${CKAN_DIR} + # Install CKAN RUN pip install -e /srv/app/src/ckan && \ - cd ${SRC_DIR}/ckan && \ cp who.ini ${APP_DIR} && \ pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ # Install default CKAN extensions @@ -160,6 +161,8 @@ RUN rm -rf /srv/app/wheels /srv/app/ext_wheels # Copy necessary scripts COPY setup/app ${APP_DIR} +WORKDIR ${APP_DIR} + # Copy the alias script for paster to be ckan so it's compatible with 2.9 COPY setup/bin/ckan /usr/bin/ckan diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index c539e08..f34eea5 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -22,19 +22,20 @@ WORKDIR ${SRC_DIR} # Set the locale RUN apt-get update -RUN apt-get install -y locales +RUN apt-get install --no-install-recommends -y locales RUN sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen RUN dpkg-reconfigure --frontend=noninteractive locales RUN update-locale LANG=${LC_ALL} # Instal apt-utils -RUN apt-get install -y \ +RUN apt-get install --no-install-recommends -y \ apt-utils # Packages to build CKAN requirements and plugins -RUN apt-get install -y \ +RUN apt-get install --no-install-recommends -y \ git \ curl \ + ca-certificates \ python \ libpq-dev \ linux-headers-generic \ @@ -91,9 +92,10 @@ ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars ENV ENVVARS_GIT_BRANCH=0.0.1 RUN apt-get update && \ - apt-get install -y \ + apt-get install --no-install-recommends -y \ git \ curl \ + ca-certificates \ python \ python-dev @@ -112,7 +114,8 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ FROM ubuntu:focal-20201106 -MAINTAINER Keitaro Inc +LABEL maintainer="Keitaro Inc " +LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan # Set timezone ENV TZ=UTC @@ -123,7 +126,7 @@ ENV LC_ALL=en_US.UTF-8 # Set the locale RUN apt-get update && \ - apt-get install -y locales && \ + apt-get install --no-install-recommends -y locales && \ sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen && \ dpkg-reconfigure --frontend=noninteractive locales && \ update-locale LANG=${LC_ALL} && \ @@ -131,18 +134,18 @@ RUN apt-get update && \ ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src +ENV CKAN_DIR=${SRC_DIR}/ckan ENV DATA_DIR=/srv/app/data ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher -WORKDIR ${APP_DIR} - # Install necessary packages to run CKAN RUN apt-get update && \ - apt-get install -y \ + apt-get install --no-install-recommends -y \ gettext \ curl \ + ca-certificates \ git \ libpq5 \ postgresql-client \ @@ -165,18 +168,19 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels COPY --from=extbuild /wheels /srv/app/ext_wheels -COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan +COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 # Create a local user and group to run the app RUN groupadd -g 92 ckan && \ useradd -rm -d /srv/app -s /bin/bash -g ckan -u 92 ckan +WORKDIR ${CKAN_DIR} + # Install CKAN RUN pip install -e /srv/app/src/ckan && \ - cd ${SRC_DIR}/ckan && \ cp who.ini ${APP_DIR} && \ pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ # Install default CKAN extensions @@ -198,6 +202,8 @@ RUN rm -rf /srv/app/wheels /srv/app/ext_wheels # Copy necessary scripts COPY setup/app ${APP_DIR} +WORKDIR ${APP_DIR} + # Copy the alias script for paster to be ckan so it's compatible with 2.9 COPY setup/bin/ckan /usr/bin/ckan diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 09ac216..a89d061 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -100,17 +100,17 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ FROM alpine:3.13 -MAINTAINER Keitaro Inc +LABEL maintainer="Keitaro Inc " +LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src +ENV CKAN_DIR=${SRC_DIR}/ckan ENV DATA_DIR=/srv/app/data ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher -WORKDIR ${APP_DIR} - # Install necessary packages to run CKAN RUN apk add --no-cache git \ bash \ @@ -136,18 +136,19 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels COPY --from=extbuild /wheels /srv/app/ext_wheels -COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan +COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ adduser -u 92 -h /srv/app -H -D -S -G ckan ckan +WORKDIR ${CKAN_DIR} + # Install CKAN RUN pip install -e /srv/app/src/ckan && \ - cd ${SRC_DIR}/ckan && \ cp who.ini ${APP_DIR} && \ pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ # Install default CKAN extensions @@ -174,6 +175,8 @@ RUN rm -rf /srv/app/wheels /srv/app/ext_wheels # Copy necessary scripts COPY setup/app ${APP_DIR} +WORKDIR ${APP_DIR} + # Create entrypoint directory for children image scripts ONBUILD RUN mkdir docker-entrypoint.d diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 6d96f94..e285623 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -22,19 +22,20 @@ WORKDIR ${SRC_DIR} # Set the locale RUN apt-get update -RUN apt-get install -y locales +RUN apt-get install --no-install-recommends -y locales RUN sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen RUN dpkg-reconfigure --frontend=noninteractive locales RUN update-locale LANG=${LC_ALL} # Instal apt-utils -RUN apt-get install -y \ +RUN apt-get install --no-install-recommends -y \ apt-utils # Packages to build CKAN requirements and plugins -RUN apt-get install -y \ +RUN apt-get install --no-install-recommends -y \ git \ curl \ + ca-certificates \ python3 \ libpq-dev \ linux-headers-generic \ @@ -97,9 +98,10 @@ ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars ENV ENVVARS_GIT_BRANCH=0.0.1 RUN apt-get update && \ - apt-get install -y \ + apt-get install --no-install-recommends -y \ git \ curl \ + ca-certificates \ python3 \ python3-dev @@ -121,7 +123,8 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ FROM ubuntu:focal-20201106 -MAINTAINER Keitaro Inc +LABEL maintainer="Keitaro Inc " +LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan # Set timezone ENV TZ=UTC @@ -132,7 +135,7 @@ ENV LC_ALL=en_US.UTF-8 # Set the locale RUN apt-get update && \ - apt-get install -y locales && \ + apt-get install --no-install-recommends -y locales && \ sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen && \ dpkg-reconfigure --frontend=noninteractive locales && \ update-locale LANG=${LC_ALL} && \ @@ -140,24 +143,24 @@ RUN apt-get update && \ ENV APP_DIR=/srv/app ENV SRC_DIR=/srv/app/src +ENV CKAN_DIR=${SRC_DIR}/ckan ENV DATA_DIR=/srv/app/data ENV PIP_SRC=${SRC_DIR} ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher -WORKDIR ${APP_DIR} - # Install necessary packages to run CKAN RUN apt-get update && \ - apt-get install -y \ + apt-get install --no-install-recommends -y \ gettext \ - curl \ + curl \ + ca-certificates \ libpq5 \ git \ - postgresql-client \ + postgresql-client \ python3 \ - python3-distutils \ - libpython3.8 \ + python3-distutils \ + libpython3.8 \ libmagic1 \ libpcre3 \ libxslt1.1 \ @@ -177,18 +180,19 @@ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels COPY --from=extbuild /wheels /srv/app/ext_wheels -COPY --from=ckanbuild /srv/app/src/ckan /srv/app/src/ckan +COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 # Create a local user and group to run the app RUN groupadd -g 92 ckan && \ useradd -rm -d /srv/app -s /bin/bash -g ckan -u 92 ckan +WORKDIR ${CKAN_DIR} + # Install CKAN RUN pip install -e /srv/app/src/ckan && \ - cd ${SRC_DIR}/ckan && \ cp who.ini ${APP_DIR} && \ pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ # Install default CKAN extensions @@ -213,6 +217,8 @@ RUN rm -rf /srv/app/wheels /srv/app/ext_wheels # Copy necessary scripts COPY setup/app ${APP_DIR} +WORKDIR ${APP_DIR} + # Create entrypoint directory for children image scripts ONBUILD RUN mkdir docker-entrypoint.d diff --git a/images/datapusher/Dockerfile b/images/datapusher/Dockerfile index 89d1ed9..86756cf 100644 --- a/images/datapusher/Dockerfile +++ b/images/datapusher/Dockerfile @@ -53,7 +53,8 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ############ FROM alpine:3.13 -MAINTAINER Keitaro Inc +LABEL maintainer="Keitaro Inc " +LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan ENV APP_DIR=/srv/app ENV JOB_CONFIG ${APP_DIR}/datapusher_settings.py @@ -67,14 +68,14 @@ RUN apk add --no-cache \ libxslt # Install pip -RUN curl -o ${src_dir}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ - python3 ${src_dir}/get-pip.py +RUN curl -o /tmp/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python3 /tmp/get-pip.py # Get artifacts from build stages COPY --from=build /wheels /srv/app/wheels # Install uwsgi and gevent -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi gevent +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ From b33fd5ebe299736c06f3a699455c18b2ea6ce1c5 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Sat, 16 Jan 2021 00:23:36 +0100 Subject: [PATCH 081/213] Add workflow for pull requests on master --- .github/workflows/pr_checks.yml | 141 ++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 .github/workflows/pr_checks.yml diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml new file mode 100644 index 0000000..38a00fa --- /dev/null +++ b/.github/workflows/pr_checks.yml @@ -0,0 +1,141 @@ +name: PR Checks + +on: + pull_request: + branches: master + workflow_dispatch: + +jobs: + build-ckan-2-9: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx + + - name: Build CKAN 2.9 alpine + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.9 + file: ./images/ckan/2.9/Dockerfile + push: false + tags: keitaro/ckan:2.9 + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + + - name: Build CKAN 2.9 ubuntu + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.9 + file: ./images/ckan/2.9/Dockerfile.focal + push: false + tags: keitaro/ckan:2.9-focal + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + + build-ckan-2-8: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Build CKAN 2.8 alpine + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.8 + file: ./images/ckan/2.8/Dockerfile + push: false + tags: keitaro/ckan:2.8 + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + + - name: Build CKAN 2.8 ubuntu + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.8 + file: ./images/ckan/2.8/Dockerfile.focal + push: false + tags: keitaro/ckan:2.8-focal + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + + build-ckan-2-7: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Build CKAN 2.7 alpine + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.7 + file: ./images/ckan/2.7/Dockerfile + push: false + tags: keitaro/ckan:2.7 + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + + - name: Build CKAN 2.7 ubuntu + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.7 + file: ./images/ckan/2.7/Dockerfile.focal + push: false + tags: keitaro/ckan:2.7-focal + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + + build-ckan-datapusher: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Build CKAN datapusher + uses: docker/build-push-action@v2 + with: + context: ./images/datapusher + file: ./images/datapusher/Dockerfile + push: false + tags: keitaro/ckandatapusher:latest + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache From 73063ff233f0650d2fa3864d5dcdc7acaecd5d24 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Sat, 16 Jan 2021 01:35:41 +0100 Subject: [PATCH 082/213] Use IMAGE_TAG to set the docker image tag --- .github/workflows/pr_checks.yml | 49 +++++++++++++++++++++++++++----- images/ckan/2.7/Dockerfile | 3 ++ images/ckan/2.7/Dockerfile.focal | 3 ++ images/ckan/2.8/Dockerfile | 3 ++ images/ckan/2.8/Dockerfile.focal | 3 ++ images/ckan/2.9/Dockerfile | 3 ++ images/ckan/2.9/Dockerfile.focal | 3 ++ images/datapusher/Dockerfile | 3 ++ 8 files changed, 63 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 38a00fa..54d94d3 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -22,23 +22,33 @@ jobs: restore-keys: | ${{ runner.os }}-buildx + - name: Get docker tag for Alpine image + id: alpine + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile)" + - name: Build CKAN 2.9 alpine uses: docker/build-push-action@v2 with: context: ./images/ckan/2.9 file: ./images/ckan/2.9/Dockerfile push: false - tags: keitaro/ckan:2.9 + tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + - name: Get docker tag for Ubuntu image + id: ubuntu + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile.focal)" + - name: Build CKAN 2.9 ubuntu uses: docker/build-push-action@v2 with: context: ./images/ckan/2.9 file: ./images/ckan/2.9/Dockerfile.focal push: false - tags: keitaro/ckan:2.9-focal + tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,mode=max,dest=/tmp/.buildx-cache @@ -58,23 +68,33 @@ jobs: restore-keys: | ${{ runner.os }}-buildx- + - name: Get docker tag for Alpine image + id: alpine + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile)" + - name: Build CKAN 2.8 alpine uses: docker/build-push-action@v2 with: context: ./images/ckan/2.8 file: ./images/ckan/2.8/Dockerfile push: false - tags: keitaro/ckan:2.8 + tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + - name: Get docker tag for Ubuntu image + id: ubuntu + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile.focal)" + - name: Build CKAN 2.8 ubuntu uses: docker/build-push-action@v2 with: context: ./images/ckan/2.8 file: ./images/ckan/2.8/Dockerfile.focal push: false - tags: keitaro/ckan:2.8-focal + tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,mode=max,dest=/tmp/.buildx-cache @@ -94,23 +114,33 @@ jobs: restore-keys: | ${{ runner.os }}-buildx- + - name: Get docker tag for Alpine image + id: alpine + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile)" + - name: Build CKAN 2.7 alpine uses: docker/build-push-action@v2 with: context: ./images/ckan/2.7 file: ./images/ckan/2.7/Dockerfile push: false - tags: keitaro/ckan:2.7 + tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + - name: Get docker tag for Ubuntu image + id: ubuntu + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile.focal)" + - name: Build CKAN 2.7 ubuntu uses: docker/build-push-action@v2 with: context: ./images/ckan/2.7 file: ./images/ckan/2.7/Dockerfile.focal push: false - tags: keitaro/ckan:2.7-focal + tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,mode=max,dest=/tmp/.buildx-cache @@ -130,12 +160,17 @@ jobs: restore-keys: | ${{ runner.os }}-buildx- + - name: Get docker tag for datapusher image + id: datapusher + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/datapusher/Dockerfile)" + - name: Build CKAN datapusher uses: docker/build-push-action@v2 with: context: ./images/datapusher file: ./images/datapusher/Dockerfile push: false - tags: keitaro/ckandatapusher:latest + tags: keitaro/ckandatapusher:${{ steps.datapusher.outputs.IMAGE_TAG }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,mode=max,dest=/tmp/.buildx-cache diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 190fd4a..3f04b83 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -3,6 +3,9 @@ ################## FROM alpine:3.13 as ckanbuild +# Used by Github Actions to tag the image with +ENV IMAGE_TAG=2.7.9 + # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git ENV GIT_BRANCH=ckan-2.7.9 diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index 8b2cb2d..b7c9cc7 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -3,6 +3,9 @@ ################## FROM ubuntu:focal-20201106 as ckanbuild +# Used by Github Actions to tag the image with +ENV IMAGE_TAG=2.7.9-focal + # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git ENV GIT_BRANCH=ckan-2.7.9 diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index c9abcbd..7380077 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -3,6 +3,9 @@ ################## FROM alpine:3.13 as ckanbuild +# Used by Github Actions to tag the image with +ENV IMAGE_TAG=2.8.6 + # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git ENV GIT_BRANCH=ckan-2.8.6 diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index f34eea5..ffcf422 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -3,6 +3,9 @@ ################## FROM ubuntu:focal-20201106 as ckanbuild +# Used by Github Actions to tag the image with +ENV IMAGE_TAG=2.8.6-focal + # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git ENV GIT_BRANCH=ckan-2.8.6 diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index a89d061..acf4b66 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -3,6 +3,9 @@ ################## FROM alpine:3.13 as ckanbuild +# Used by Github Actions to tag the image with +ENV IMAGE_TAG=2.9.1 + # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git ENV GIT_BRANCH=ckan-2.9.1 diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index e285623..a0fa8f5 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -3,6 +3,9 @@ ################## FROM ubuntu:focal-20201106 as ckanbuild +# Used by Github Actions to tag the image with +ENV IMAGE_TAG=2.9.1-focal + # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git ENV GIT_BRANCH=ckan-2.9.1 diff --git a/images/datapusher/Dockerfile b/images/datapusher/Dockerfile index 86756cf..3385d40 100644 --- a/images/datapusher/Dockerfile +++ b/images/datapusher/Dockerfile @@ -3,6 +3,9 @@ ############# FROM alpine:3.13 as build +# Used by Github Actions to tag the image with +ENV IMAGE_TAG=0.0.17 + # Set datapusher version to build ENV GIT_URL https://github.com/keitaroinc/datapusher.git ENV GIT_BRANCH master From 894070361653805f88e378cbbe30049c7e2b6408 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Sat, 16 Jan 2021 02:15:03 +0100 Subject: [PATCH 083/213] Add automatic push to GHCR and dockerhub on push to master and set GHCR default --- .github/workflows/master_merge.yml | 241 ++++++++++++++++++++++++ .github/workflows/pr_checks.yml | 1 - Readme.md | 12 +- compose/docker-compose.yml | 4 +- examples/harvest/Dockerfile | 4 +- examples/harvest/docker-compose.yml | 2 +- examples/s3filestore/Dockerfile | 4 +- examples/s3filestore/docker-compose.yml | 2 +- 8 files changed, 257 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/master_merge.yml diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml new file mode 100644 index 0000000..74af5af --- /dev/null +++ b/.github/workflows/master_merge.yml @@ -0,0 +1,241 @@ +name: Build and push docker images + +on: + push: + branches: master + +jobs: + build-ckan-2-9: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.CR_PAT }} + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx + + - name: Get docker tag for Alpine image + id: alpine + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile)" + + - name: Build and push CKAN 2.9 alpine + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.9 + file: ./images/ckan/2.9/Dockerfile + push: true + tags: | + keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + ghcr.io/keitaroinc/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + + - name: Get docker tag for Ubuntu image + id: ubuntu + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile.focal)" + + - name: Build and push CKAN 2.9 ubuntu + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.9 + file: ./images/ckan/2.9/Dockerfile.focal + push: true + tags: | + keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + ghcr.io/keitaroinc/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + + build-ckan-2-8: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.CR_PAT }} + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Get docker tag for Alpine image + id: alpine + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile)" + + - name: Build and push CKAN 2.8 alpine + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.8 + file: ./images/ckan/2.8/Dockerfile + push: true + tags: | + keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + ghcr.io/keitaroinc/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + + - name: Get docker tag for Ubuntu image + id: ubuntu + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile.focal)" + + - name: Build and push CKAN 2.8 ubuntu + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.8 + file: ./images/ckan/2.8/Dockerfile.focal + push: true + tags: | + keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + ghcr.io/keitaroinc/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + + build-ckan-2-7: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.CR_PAT }} + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Get docker tag for Alpine image + id: alpine + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile)" + + - name: Build and push CKAN 2.7 alpine + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.7 + file: ./images/ckan/2.7/Dockerfile + push: true + tags: | + keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + ghcr.io/keitaroinc/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + + - name: Get docker tag for Ubuntu image + id: ubuntu + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile.focal)" + + - name: Build CKAN 2.7 ubuntu + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.7 + file: ./images/ckan/2.7/Dockerfile.focal + push: true + tags: | + keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + ghcr.io/keitaroinc/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + + build-ckan-datapusher: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.CR_PAT }} + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Get docker tag for datapusher image + id: datapusher + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/datapusher/Dockerfile)" + + - name: Build and push CKAN datapusher + uses: docker/build-push-action@v2 + with: + context: ./images/datapusher + file: ./images/datapusher/Dockerfile + push: true + tags: | + keitaro/datapusher:${{ steps.datapusher.outputs.IMAGE_TAG }} + ghcr.io/keitaroinc/datapusher:${{ steps.datapusher.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 54d94d3..34a1665 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -3,7 +3,6 @@ name: PR Checks on: pull_request: branches: master - workflow_dispatch: jobs: build-ckan-2-9: diff --git a/Readme.md b/Readme.md index ff95437..2ef79ae 100644 --- a/Readme.md +++ b/Readme.md @@ -8,6 +8,10 @@ We build and publish docker images built using this repository to Dockerhub: - [CKAN docker images](https://hub.docker.com/r/keitaro/ckan). - [Datapusher docker images](https://hub.docker.com/r/keitaro/ckan-datapusher) +and Github Container Registry: +- [CKAN docker images on GHCR](https://github.com/orgs/keitaroinc/packages/container/package/ckan) +- [Datapusher docker images on GHCR](https://github.com/orgs/keitaroinc/packages/container/package/datapusher) + Looking to run CKAN on Kubernetes? Check out our [CKAN Helm Chart](https://github.com/keitaroinc/ckan-helm)! ## Overview @@ -40,7 +44,7 @@ We recommend to use a multi-stage approach to extend the docker images that we p ################### ### Extensions #### ################### -FROM keitaro/ckan:2.9.1 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.1 as extbuild # Switch to the root user USER root @@ -54,7 +58,7 @@ RUN pip wheel --wheel-dir=/wheels git+https://github.com/acmecorp/ckanext-acme@0 ############ ### MAIN ### ############ -FROM keitaro/ckan:2.9.1 +FROM ghcr.io/keitaroinc/ckan:2.9.1 # Add the custom extensions to the plugins list ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher acme @@ -84,9 +88,9 @@ You can add scripts to CKAN custom images and copy them to the *docker-afterinit ## Build To build a CKAN image run: ```sh -docker build --tag keitaro/ckan:2.9.1 images/ckan/2.9 +docker build --tag ghcr.io/keitaroinc/ckan:2.9.1 images/ckan/2.9 ``` -The –-tag keitaro/ckan:2.9.1 flag sets the image name to ketiaro/ckan:2.9.1 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. +The –-tag ghcr.io/keitaroinc/ckan:2.9.1 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.1 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. ## Upload to DockerHub >*It's recommended to upload built images to DockerHub* diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index 675fbf8..c5c17b6 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -9,7 +9,7 @@ volumes: services: ckan: container_name: ckan - image: keitaro/ckan:${CKAN_VERSION} + image: ghcr.io/keitaroinc/ckan:${CKAN_VERSION} networks: - frontend - backend @@ -34,7 +34,7 @@ services: datapusher: container_name: datapusher - image: keitaro/ckan-datapusher:${DATAPUSHER_VERSION} + image: ghcr.io/keitaroinc/datapusher:${DATAPUSHER_VERSION} networks: - frontend - backend diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index df3202f..49a345b 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM keitaro/ckan:2.9.1 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.1 as extbuild # Locations and tags, please use specific tags or revisions ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest @@ -28,7 +28,7 @@ USER ckan ############ ### MAIN ### ############ -FROM keitaro/ckan:2.9.1 +FROM ghcr.io/keitaroinc/ckan:2.9.1 LABEL maintainer="Keitaro Inc " diff --git a/examples/harvest/docker-compose.yml b/examples/harvest/docker-compose.yml index fc2f961..7ad2d70 100644 --- a/examples/harvest/docker-compose.yml +++ b/examples/harvest/docker-compose.yml @@ -133,7 +133,7 @@ services: datapusher: container_name: datapusher - image: keitaro/ckan-datapusher:${DATAPUSHER_VERSION} + image: ghcr.io/keitaroinc/datapusher:${DATAPUSHER_VERSION} networks: - frontend - backend diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index 7e45e25..14d3d8d 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM keitaro/ckan:2.8.6 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.8.6 as extbuild # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore @@ -20,7 +20,7 @@ USER ckan ############ ### MAIN ### ############ -FROM keitaro/ckan:2.8.6 +FROM ghcr.io/keitaroinc/ckan:2.8.6 LABEL maintainer="Keitaro Inc " diff --git a/examples/s3filestore/docker-compose.yml b/examples/s3filestore/docker-compose.yml index a2eb487..45d8359 100644 --- a/examples/s3filestore/docker-compose.yml +++ b/examples/s3filestore/docker-compose.yml @@ -33,7 +33,7 @@ services: datapusher: container_name: datapusher - image: keitaro/ckan-datapusher:${DATAPUSHER_VERSION} + image: ghcr.io/keitaroinc/datapusher:${DATAPUSHER_VERSION} networks: - frontend - backend From d4a86326d8726075ecb3c576585be02f3a209a50 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Sat, 16 Jan 2021 02:42:37 +0100 Subject: [PATCH 084/213] Fix caching of docker layers --- .github/workflows/master_merge.yml | 58 ++++++++++++++++-------------- .github/workflows/pr_checks.yml | 58 ++++++++++++++++-------------- Readme.md | 6 +++- 3 files changed, 69 insertions(+), 53 deletions(-) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index 74af5af..bd8de38 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -29,10 +29,12 @@ jobs: - name: Cache Docker layers uses: actions/cache@v2 with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} + path: | + /tmp/.buildx-cache-alpine-2-9 + /tmp/.buildx-cache-ubuntu-2-9 + key: ${{ runner.os }}-buildx-2-9-${{ github.sha }} restore-keys: | - ${{ runner.os }}-buildx + ${{ runner.os }}-buildx-2-9 - name: Get docker tag for Alpine image id: alpine @@ -48,8 +50,8 @@ jobs: tags: | keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} ghcr.io/keitaroinc/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-9 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-9 - name: Get docker tag for Ubuntu image id: ubuntu @@ -65,8 +67,8 @@ jobs: tags: | keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} ghcr.io/keitaroinc/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-9 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-9 build-ckan-2-8: runs-on: ubuntu-latest @@ -92,10 +94,12 @@ jobs: - name: Cache Docker layers uses: actions/cache@v2 with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} + path: | + /tmp/.buildx-cache-alpine-2-8 + /tmp/.buildx-cache-ubuntu-2-8 + key: ${{ runner.os }}-buildx-2-8-${{ github.sha }} restore-keys: | - ${{ runner.os }}-buildx- + ${{ runner.os }}-buildx-2-8 - name: Get docker tag for Alpine image id: alpine @@ -111,8 +115,8 @@ jobs: tags: | keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} ghcr.io/keitaroinc/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-8 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-8 - name: Get docker tag for Ubuntu image id: ubuntu @@ -128,8 +132,8 @@ jobs: tags: | keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} ghcr.io/keitaroinc/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-8 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-8 build-ckan-2-7: runs-on: ubuntu-latest @@ -155,10 +159,12 @@ jobs: - name: Cache Docker layers uses: actions/cache@v2 with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} + path: | + /tmp/.buildx-cache-alpine-2-7 + /tmp/.buildx-cache-ubuntu-2-7 + key: ${{ runner.os }}-buildx-2-7-${{ github.sha }} restore-keys: | - ${{ runner.os }}-buildx- + ${{ runner.os }}-buildx-2-7 - name: Get docker tag for Alpine image id: alpine @@ -174,8 +180,8 @@ jobs: tags: | keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} ghcr.io/keitaroinc/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-7 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-7 - name: Get docker tag for Ubuntu image id: ubuntu @@ -191,8 +197,8 @@ jobs: tags: | keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} ghcr.io/keitaroinc/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-7 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-7 build-ckan-datapusher: runs-on: ubuntu-latest @@ -218,10 +224,10 @@ jobs: - name: Cache Docker layers uses: actions/cache@v2 with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} + path: /tmp/.buildx-cache-datapusher + key: ${{ runner.os }}-buildx-datapusher-${{ github.sha }} restore-keys: | - ${{ runner.os }}-buildx- + ${{ runner.os }}-buildx-datapusher - name: Get docker tag for datapusher image id: datapusher @@ -237,5 +243,5 @@ jobs: tags: | keitaro/datapusher:${{ steps.datapusher.outputs.IMAGE_TAG }} ghcr.io/keitaroinc/datapusher:${{ steps.datapusher.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-datapusher + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-datapusher diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 34a1665..092597c 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -16,10 +16,12 @@ jobs: - name: Cache Docker layers uses: actions/cache@v2 with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} + path: | + /tmp/.buildx-cache-alpine-2-9 + /tmp/.buildx-cache-ubuntu-2-9 + key: ${{ runner.os }}-buildx-2-9-${{ github.sha }} restore-keys: | - ${{ runner.os }}-buildx + ${{ runner.os }}-buildx-2-9 - name: Get docker tag for Alpine image id: alpine @@ -33,8 +35,8 @@ jobs: file: ./images/ckan/2.9/Dockerfile push: false tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-9 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-9 - name: Get docker tag for Ubuntu image id: ubuntu @@ -48,8 +50,8 @@ jobs: file: ./images/ckan/2.9/Dockerfile.focal push: false tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-9 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-9 build-ckan-2-8: runs-on: ubuntu-latest @@ -62,10 +64,12 @@ jobs: - name: Cache Docker layers uses: actions/cache@v2 with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} + path: | + /tmp/.buildx-cache-alpine-2-8 + /tmp/.buildx-cache-ubuntu-2-8 + key: ${{ runner.os }}-buildx-2-8-${{ github.sha }} restore-keys: | - ${{ runner.os }}-buildx- + ${{ runner.os }}-buildx-2-8 - name: Get docker tag for Alpine image id: alpine @@ -79,8 +83,8 @@ jobs: file: ./images/ckan/2.8/Dockerfile push: false tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-8 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-8 - name: Get docker tag for Ubuntu image id: ubuntu @@ -94,8 +98,8 @@ jobs: file: ./images/ckan/2.8/Dockerfile.focal push: false tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-8 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-8 build-ckan-2-7: runs-on: ubuntu-latest @@ -108,10 +112,12 @@ jobs: - name: Cache Docker layers uses: actions/cache@v2 with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} + path: | + /tmp/.buildx-cache-alpine-2-7 + /tmp/.buildx-cache-ubuntu-2-7 + key: ${{ runner.os }}-buildx-2-7-${{ github.sha }} restore-keys: | - ${{ runner.os }}-buildx- + ${{ runner.os }}-buildx-2-7 - name: Get docker tag for Alpine image id: alpine @@ -125,8 +131,8 @@ jobs: file: ./images/ckan/2.7/Dockerfile push: false tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-7 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-7 - name: Get docker tag for Ubuntu image id: ubuntu @@ -140,8 +146,8 @@ jobs: file: ./images/ckan/2.7/Dockerfile.focal push: false tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-7 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-7 build-ckan-datapusher: runs-on: ubuntu-latest @@ -154,10 +160,10 @@ jobs: - name: Cache Docker layers uses: actions/cache@v2 with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} + path: /tmp/.buildx-cache-datapusher + key: ${{ runner.os }}-buildx-datapusher-${{ github.sha }} restore-keys: | - ${{ runner.os }}-buildx- + ${{ runner.os }}-buildx-datapusher - name: Get docker tag for datapusher image id: datapusher @@ -171,5 +177,5 @@ jobs: file: ./images/datapusher/Dockerfile push: false tags: keitaro/ckandatapusher:${{ steps.datapusher.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache + cache-from: type=local,src=/tmp/.buildx-cache-datapusher + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-datapusher diff --git a/Readme.md b/Readme.md index 2ef79ae..74f0b75 100644 --- a/Readme.md +++ b/Readme.md @@ -15,7 +15,11 @@ and Github Container Registry: Looking to run CKAN on Kubernetes? Check out our [CKAN Helm Chart](https://github.com/keitaroinc/ckan-helm)! ## Overview -All images are based on [Alpine Linux](https://alpinelinux.org/) and include only required extensions to start a CKAN instance. The docker images are built using a multi-stage docker approach in order to produce slim production grade docker images with the right libraries and configuration. This multi-stage approach allows us to build python binary wheels in the build stages that later on we install in the main stage. +Images are provided in two flavors: +- [Alpine Linux](https://alpinelinux.org/) based images +- [Ubuntu Focal](https://ubuntu.com/) based images are the ones ending with `-focal` in the tag name + +The Docker containers include only the required extensions to start a CKAN instance. The docker images are built using a multi-stage docker approach in order to produce slim production grade docker images with the right libraries and configuration. This multi-stage approach allows us to build python binary wheels in the build stages that later on we install in the main stage. Directory layout: - [compose](./compose) - contains a docker-compose setup allowing users to spin up a CKAN setup easily using [docker-compose](https://docs.docker.com/compose/) From a5416fb71f4911df944a736e5953de27dc597838 Mon Sep 17 00:00:00 2001 From: Blagoja Stojkoski Date: Wed, 20 Jan 2021 15:44:37 +0100 Subject: [PATCH 085/213] Workflow rename and badge (#37) * fixes workflow typos * badge test * renamed build action and added badge --- .github/workflows/master_merge.yml | 10 +++++----- Readme.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index bd8de38..af3ac68 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -1,4 +1,4 @@ -name: Build and push docker images +name: Docker Image Build on: push: @@ -19,7 +19,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry + - name: Login to GitHub Container Registry uses: docker/login-action@v1 with: registry: ghcr.io @@ -84,7 +84,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry + - name: Login to GitHub Container Registry uses: docker/login-action@v1 with: registry: ghcr.io @@ -149,7 +149,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry + - name: Login to GitHub Container Registry uses: docker/login-action@v1 with: registry: ghcr.io @@ -214,7 +214,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry + - name: Login to GitHub Container Registry uses: docker/login-action@v1 with: registry: ghcr.io diff --git a/Readme.md b/Readme.md index 74f0b75..473d807 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,6 @@ # Dockerized CKAN -[![License][]][1] [![Docker Pulls][]][2] [![Chat on Gitter][]][3] +[![build-status](https://github.com/keitaroinc/docker-ckan/workflows/Docker%20Image%20Build/badge.svg?branch=master)](https://github.com/keitaroinc/docker-ckan/actions) [![License][]][1] [![Docker Pulls][]][2] [![Chat on Gitter][]][3] This repository contains base docker images, examples and docker-compose used to build and run CKAN. From 2f9500cda7845b129c985089cce0dd979c496be2 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Mon, 25 Jan 2021 14:10:23 +0100 Subject: [PATCH 086/213] Pin the version of pip since pip dropped python2 support in 21.0 --- images/ckan/2.7/Dockerfile | 6 +++--- images/ckan/2.7/Dockerfile.focal | 6 +++--- images/ckan/2.8/Dockerfile | 6 +++--- images/ckan/2.8/Dockerfile.focal | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 3f04b83..afe4e45 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -46,7 +46,7 @@ RUN apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.6/ma RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ python ${SRC_DIR}/get-pip.py # Fetch and build CKAN and requirements @@ -87,7 +87,7 @@ RUN apk add --no-cache \ RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ python ${SRC_DIR}/get-pip.py # Fetch and build the default CKAN extensions @@ -129,7 +129,7 @@ RUN apk add --no-cache \ mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ python ${SRC_DIR}/get-pip.py # Get artifacts from build stages diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index b7c9cc7..70675dc 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -71,7 +71,7 @@ RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave / RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ python ${SRC_DIR}/get-pip.py # Fetch and build CKAN and requirements @@ -114,7 +114,7 @@ RUN apt-get update && \ RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ python ${SRC_DIR}/get-pip.py # Fetch and build the default CKAN extensions @@ -180,7 +180,7 @@ RUN apt-get update && \ mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ python ${SRC_DIR}/get-pip.py # Get artifacts from build stages diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 7380077..a79ed5e 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -42,7 +42,7 @@ RUN apk add --no-cache \ RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ python ${SRC_DIR}/get-pip.py # Fetch and build CKAN and requirements @@ -83,7 +83,7 @@ RUN apk add --no-cache \ RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ python ${SRC_DIR}/get-pip.py # Fetch and build the default CKAN extensions @@ -122,7 +122,7 @@ RUN apk add --no-cache git \ mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ python ${SRC_DIR}/get-pip.py # Get artifacts from build stages diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index ffcf422..9c98e03 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -63,7 +63,7 @@ RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave / RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ python ${SRC_DIR}/get-pip.py # Fetch and build CKAN and requirements @@ -106,7 +106,7 @@ RUN apt-get update && \ RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ python ${SRC_DIR}/get-pip.py # Fetch and build the default CKAN extensions @@ -165,7 +165,7 @@ RUN apt-get update && \ mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ python ${SRC_DIR}/get-pip.py # Get artifacts from build stages From ec778aba876c17925de78822c8d8081c0a86d2df Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Mon, 25 Jan 2021 14:22:34 +0100 Subject: [PATCH 087/213] Pin pip to 20.3.3 due to issue with 20.3.4 --- images/ckan/2.7/Dockerfile | 6 +++--- images/ckan/2.7/Dockerfile.focal | 6 +++--- images/ckan/2.8/Dockerfile | 6 +++--- images/ckan/2.8/Dockerfile.focal | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index afe4e45..fd61b9d 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -47,7 +47,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan @@ -88,7 +88,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -130,7 +130,7 @@ RUN apk add --no-cache \ # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index 70675dc..af263ae 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -72,7 +72,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan @@ -115,7 +115,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -181,7 +181,7 @@ RUN apt-get update && \ # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index a79ed5e..ae5dc10 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -43,7 +43,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan @@ -84,7 +84,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -123,7 +123,7 @@ RUN apk add --no-cache git \ # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index 9c98e03..27f1ded 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -64,7 +64,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan @@ -107,7 +107,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -166,7 +166,7 @@ RUN apt-get update && \ # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels From 69ec6e65b6d0d7a39db31b17fe20c4388dc53a3c Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 3 Feb 2021 16:06:23 +0100 Subject: [PATCH 088/213] New CKAN patches 2.7.10, 2.8.7 and 2.9.2 --- Readme.md | 8 ++++---- compose/.env | 2 +- compose/solr/solrconfig-2.7.10.xml | 1 + compose/solr/solrconfig-2.7.9.xml | 1 - .../{solrconfig-2.8.6.xml => solrconfig-2.8.7.xml} | 0 .../{solrconfig-2.9.1.xml => solrconfig-2.9.2.xml} | 0 examples/s3filestore/.env | 2 +- examples/s3filestore/Dockerfile | 4 ++-- images/ckan/2.7/Dockerfile | 4 ++-- images/ckan/2.7/Dockerfile.focal | 4 ++-- images/ckan/2.8/Dockerfile | 4 ++-- images/ckan/2.8/Dockerfile.focal | 4 ++-- images/ckan/2.9/Dockerfile | 4 ++-- images/ckan/2.9/Dockerfile.focal | 4 ++-- .../ckan/2.9/patches/00_get_limit_type_check.patch | 13 ------------- 15 files changed, 21 insertions(+), 34 deletions(-) create mode 120000 compose/solr/solrconfig-2.7.10.xml delete mode 120000 compose/solr/solrconfig-2.7.9.xml rename compose/solr/{solrconfig-2.8.6.xml => solrconfig-2.8.7.xml} (100%) rename compose/solr/{solrconfig-2.9.1.xml => solrconfig-2.9.2.xml} (100%) delete mode 100644 images/ckan/2.9/patches/00_get_limit_type_check.patch diff --git a/Readme.md b/Readme.md index 473d807..be7f541 100644 --- a/Readme.md +++ b/Readme.md @@ -48,7 +48,7 @@ We recommend to use a multi-stage approach to extend the docker images that we p ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.1 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.2 as extbuild # Switch to the root user USER root @@ -62,7 +62,7 @@ RUN pip wheel --wheel-dir=/wheels git+https://github.com/acmecorp/ckanext-acme@0 ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.1 +FROM ghcr.io/keitaroinc/ckan:2.9.2 # Add the custom extensions to the plugins list ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher acme @@ -92,9 +92,9 @@ You can add scripts to CKAN custom images and copy them to the *docker-afterinit ## Build To build a CKAN image run: ```sh -docker build --tag ghcr.io/keitaroinc/ckan:2.9.1 images/ckan/2.9 +docker build --tag ghcr.io/keitaroinc/ckan:2.9.2 images/ckan/2.9 ``` -The –-tag ghcr.io/keitaroinc/ckan:2.9.1 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.1 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. +The –-tag ghcr.io/keitaroinc/ckan:2.9.2 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.2 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. ## Upload to DockerHub >*It's recommended to upload built images to DockerHub* diff --git a/compose/.env b/compose/.env index 514724c..89a9c51 100644 --- a/compose/.env +++ b/compose/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.1 +CKAN_VERSION=2.9.2 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/solr/solrconfig-2.7.10.xml b/compose/solr/solrconfig-2.7.10.xml new file mode 120000 index 0000000..207d108 --- /dev/null +++ b/compose/solr/solrconfig-2.7.10.xml @@ -0,0 +1 @@ +solrconfig-2.8.7.xml \ No newline at end of file diff --git a/compose/solr/solrconfig-2.7.9.xml b/compose/solr/solrconfig-2.7.9.xml deleted file mode 120000 index 9859b31..0000000 --- a/compose/solr/solrconfig-2.7.9.xml +++ /dev/null @@ -1 +0,0 @@ -solrconfig-2.8.6.xml \ No newline at end of file diff --git a/compose/solr/solrconfig-2.8.6.xml b/compose/solr/solrconfig-2.8.7.xml similarity index 100% rename from compose/solr/solrconfig-2.8.6.xml rename to compose/solr/solrconfig-2.8.7.xml diff --git a/compose/solr/solrconfig-2.9.1.xml b/compose/solr/solrconfig-2.9.2.xml similarity index 100% rename from compose/solr/solrconfig-2.9.1.xml rename to compose/solr/solrconfig-2.9.2.xml diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env index d66972b..d519cb2 100644 --- a/examples/s3filestore/.env +++ b/examples/s3filestore/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.8.6 +CKAN_VERSION=2.8.7 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index 14d3d8d..8f58d14 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.8.6 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.8.7 as extbuild # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore @@ -20,7 +20,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.8.6 +FROM ghcr.io/keitaroinc/ckan:2.8.7 LABEL maintainer="Keitaro Inc " diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index fd61b9d..2ed123a 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.13 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.7.9 +ENV IMAGE_TAG=2.7.10 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.7.9 +ENV GIT_BRANCH=ckan-2.7.10 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index af263ae..b496b21 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20201106 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.7.9-focal +ENV IMAGE_TAG=2.7.10-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.7.9 +ENV GIT_BRANCH=ckan-2.7.10 # Set timezone ENV TZ=UTC diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index ae5dc10..3bdf6e0 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.13 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.8.6 +ENV IMAGE_TAG=2.8.7 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.6 +ENV GIT_BRANCH=ckan-2.8.7 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index 27f1ded..ac4a0ba 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20201106 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.8.6-focal +ENV IMAGE_TAG=2.8.7-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.6 +ENV GIT_BRANCH=ckan-2.8.7 # Set timezone ENV TZ=UTC diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index acf4b66..0baa7c5 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.13 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.1 +ENV IMAGE_TAG=2.9.2 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.1 +ENV GIT_BRANCH=ckan-2.9.2 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index a0fa8f5..2521306 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20201106 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.1-focal +ENV IMAGE_TAG=2.9.2-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.1 +ENV GIT_BRANCH=ckan-2.9.2 # Set timezone ENV TZ=UTC diff --git a/images/ckan/2.9/patches/00_get_limit_type_check.patch b/images/ckan/2.9/patches/00_get_limit_type_check.patch deleted file mode 100644 index 6adc118..0000000 --- a/images/ckan/2.9/patches/00_get_limit_type_check.patch +++ /dev/null @@ -1,13 +0,0 @@ ---- ckan/ckan/logic/action/get.py 2020-12-09 12:05:49.800909223 +0100 -+++ get_limit_type_check.py 2020-12-09 12:11:58.404000000 +0100 -@@ -371,8 +371,8 @@ - 'ckan.group_and_organization_list_all_fields_max', 25) - else: - max_limit = config.get('ckan.group_and_organization_list_max', 1000) -- if limit is None or limit > max_limit: -- limit = max_limit -+ if limit is None or int(limit) > max_limit: -+ limit = str(max_limit) - - # order_by deprecated in ckan 1.8 - # if it is supplied and sort isn't use order_by and raise a warning From 08c10f2dc600ee0eeff503766830c4d4c2b6adcd Mon Sep 17 00:00:00 2001 From: Blagoja Stojkoski Date: Thu, 18 Feb 2021 14:36:39 +0100 Subject: [PATCH 089/213] fixes resource replace upload (#41) * fixes resource replace upload * Add rust and cargo in order to build cryptography Co-authored-by: Marko Bocevski --- .../patches/01_patch_resource_replace_upload.patch | 11 +++++++++++ images/datapusher/Dockerfile | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 images/ckan/2.9/patches/01_patch_resource_replace_upload.patch diff --git a/images/ckan/2.9/patches/01_patch_resource_replace_upload.patch b/images/ckan/2.9/patches/01_patch_resource_replace_upload.patch new file mode 100644 index 0000000..3f11fac --- /dev/null +++ b/images/ckan/2.9/patches/01_patch_resource_replace_upload.patch @@ -0,0 +1,11 @@ +--- ckan/ckan/logic/action/update.py 2021-02-17 16:46:55.673578728 +0100 ++++ ckan/ckan/logic/action/update-edit.py 2021-02-17 16:47:28.905879170 +0100 +@@ -929,7 +929,7 @@ + + ''' + model = context['model'] +- session = model.Session ++ session = model.meta.create_local_session() + context['session'] = session + + user = context['user'] diff --git a/images/datapusher/Dockerfile b/images/datapusher/Dockerfile index 3385d40..4894864 100644 --- a/images/datapusher/Dockerfile +++ b/images/datapusher/Dockerfile @@ -33,7 +33,9 @@ RUN apk add --no-cache \ libffi-dev \ openssl-dev \ libxml2-dev \ - libxslt-dev + libxslt-dev \ + rust \ + cargo # Create the src directory RUN mkdir -p ${SRC_DIR} From 21c891865d8aade1e3e2e8b8b9d0293060602db9 Mon Sep 17 00:00:00 2001 From: Blagoja Stojkoski Date: Thu, 18 Feb 2021 15:02:23 +0100 Subject: [PATCH 090/213] Patches for connecting to Azure PSQL (#42) * * patches in code to handle user@host format in the Azure PSQL connection string * adds code for handling new connection string format in prerun.py * * fixes path in the postgres username patch * cleans up prerun.py imports --- .../ckan/2.9/patches/00_patch_sql_url.patch | 11 +++++++++ .../02_patch_postgres_username_split.patch | 11 +++++++++ images/ckan/2.9/setup/app/prerun.py | 23 ++++++++++++++++--- 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 images/ckan/2.9/patches/00_patch_sql_url.patch create mode 100644 images/ckan/2.9/patches/02_patch_postgres_username_split.patch diff --git a/images/ckan/2.9/patches/00_patch_sql_url.patch b/images/ckan/2.9/patches/00_patch_sql_url.patch new file mode 100644 index 0000000..46a7135 --- /dev/null +++ b/images/ckan/2.9/patches/00_patch_sql_url.patch @@ -0,0 +1,11 @@ +--- ckan/ckan/model/__init__.py 2021-02-16 14:47:06.168327441 +0100 ++++ ckan/ckan/model/__init__.py 2021-02-16 14:48:00.740780218 +0100 +@@ -266,7 +266,7 @@ + self.reset_alembic_output() + alembic_config = AlembicConfig(self._alembic_ini) + alembic_config.set_main_option( +- "sqlalchemy.url", str(self.metadata.bind.url) ++ "sqlalchemy.url", str(self.metadata.bind.url).replace('%', '%%') + ) + try: + sqlalchemy_migrate_version = self.metadata.bind.execute( diff --git a/images/ckan/2.9/patches/02_patch_postgres_username_split.patch b/images/ckan/2.9/patches/02_patch_postgres_username_split.patch new file mode 100644 index 0000000..780b432 --- /dev/null +++ b/images/ckan/2.9/patches/02_patch_postgres_username_split.patch @@ -0,0 +1,11 @@ +--- ckan/ckanext/datastore/backend/postgres.py 2021-02-18 11:01:56.692267462 +0100 ++++ ckan/ckanext/datastore/backend/postgres-patch.py 2021-02-18 13:45:16.033193435 +0100 +@@ -1690,7 +1690,7 @@ + read only user. + ''' + write_connection = self._get_write_engine().connect() +- read_connection_user = sa_url.make_url(self.read_url).username ++ read_connection_user = sa_url.make_url(self.read_url).username.split("@")[0] + + drop_foo_sql = u'DROP TABLE IF EXISTS _foo' + diff --git a/images/ckan/2.9/setup/app/prerun.py b/images/ckan/2.9/setup/app/prerun.py index 60b22d6..ca4598f 100644 --- a/images/ckan/2.9/setup/app/prerun.py +++ b/images/ckan/2.9/setup/app/prerun.py @@ -2,6 +2,7 @@ import os import sys import subprocess import psycopg2 +from sqlalchemy.engine.url import make_url import urllib.request, urllib.error, urllib.parse import re @@ -23,7 +24,14 @@ def check_db_connection(retry=None): conn_str = os.environ.get('CKAN_SQLALCHEMY_URL', '') try: - connection = psycopg2.connect(conn_str) + db_user = make_url(conn_str).username + db_passwd = make_url(conn_str).password + db_host = make_url(conn_str).host + db_name = make_url(conn_str).database + connection = psycopg2.connect(user=db_user, + host=db_host, + password=db_passwd, + database=db_name) except psycopg2.Error as e: print((str(e))) @@ -96,7 +104,14 @@ def init_datastore(): datastore_perms_command = ['ckan', '-c', ckan_ini, 'datastore', 'set-permissions'] - connection = psycopg2.connect(conn_str) + db_user = make_url(conn_str).username + db_passwd = make_url(conn_str).password + db_host = make_url(conn_str).host + db_name = make_url(conn_str).database + connection = psycopg2.connect(user=db_user, + host=db_host, + password=db_passwd, + database=db_name) cursor = connection.cursor() print('[prerun] Initializing datastore db - start') @@ -106,8 +121,10 @@ def init_datastore(): stdout=subprocess.PIPE) perms_sql = datastore_perms.stdout.read() + perms_sql = perms_sql.decode('utf-8') + perms_sql = perms_sql.replace("@"+db_host, "") # Remove internal pg command as psycopg2 does not like it - perms_sql = re.sub('\\\\connect \"(.*)\"', '', perms_sql.decode('utf-8')) + perms_sql = re.sub('\\\\connect \"(.*)\"', '', perms_sql) cursor.execute(perms_sql) for notice in connection.notices: print(notice) From af4a5353a4a8233b57078450650992ee2f3aacd1 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 12 Mar 2021 17:36:12 +0100 Subject: [PATCH 091/213] Fix ckan config-tool command in README --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index be7f541..15318d8 100644 --- a/Readme.md +++ b/Readme.md @@ -74,7 +74,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels # Install and enable the custom extensions RUN pip install --no-index --find-links=/srv/app/ext_wheels ckanext-acme && \ - ckan -c ${APP_DIR}/production.ini config-tool "ckan.plugins = ${CKAN__PLUGINS}" && \ + ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ chown -R ckan:ckan /srv/app # Remove wheels From 5b2c0b95a6ce154847bff8384d84bb8cf5361e63 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 24 Mar 2021 17:40:15 +0100 Subject: [PATCH 092/213] Fix pip url for python 2.7 --- examples/harvest/Dockerfile | 4 ++-- images/ckan/2.7/Dockerfile | 12 ++++++------ images/ckan/2.7/Dockerfile.focal | 12 ++++++------ images/ckan/2.8/Dockerfile | 12 ++++++------ images/ckan/2.8/Dockerfile.focal | 12 ++++++------ 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index 49a345b..9ba0f86 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.1 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.2 as extbuild # Locations and tags, please use specific tags or revisions ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest @@ -28,7 +28,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.1 +FROM ghcr.io/keitaroinc/ckan:2.9.2 LABEL maintainer="Keitaro Inc " diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 2ed123a..3435bc3 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -46,8 +46,8 @@ RUN apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.6/ma RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py 'pip==20.3.3' +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ + python ${SRC_DIR}/get-pip.py # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan @@ -87,8 +87,8 @@ RUN apk add --no-cache \ RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py 'pip==20.3.3' +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ + python ${SRC_DIR}/get-pip.py # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -129,8 +129,8 @@ RUN apk add --no-cache \ mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py 'pip==20.3.3' +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ + python ${SRC_DIR}/get-pip.py # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index b496b21..d22a37f 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -71,8 +71,8 @@ RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave / RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py 'pip==20.3.3' +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ + python ${SRC_DIR}/get-pip.py # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan @@ -114,8 +114,8 @@ RUN apt-get update && \ RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py 'pip==20.3.3' +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ + python ${SRC_DIR}/get-pip.py # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -180,8 +180,8 @@ RUN apt-get update && \ mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py 'pip==20.3.3' +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ + python ${SRC_DIR}/get-pip.py # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 3bdf6e0..281995d 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -42,8 +42,8 @@ RUN apk add --no-cache \ RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py 'pip==20.3.3' +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ + python ${SRC_DIR}/get-pip.py # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan @@ -83,8 +83,8 @@ RUN apk add --no-cache \ RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py 'pip==20.3.3' +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ + python ${SRC_DIR}/get-pip.py # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -122,8 +122,8 @@ RUN apk add --no-cache git \ mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py 'pip==20.3.3' +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ + python ${SRC_DIR}/get-pip.py # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index ac4a0ba..37e5baa 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -63,8 +63,8 @@ RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave / RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py 'pip==20.3.3' +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ + python ${SRC_DIR}/get-pip.py # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan @@ -106,8 +106,8 @@ RUN apt-get update && \ RUN mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py 'pip==20.3.3' +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ + python ${SRC_DIR}/get-pip.py # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -165,8 +165,8 @@ RUN apt-get update && \ mkdir -p ${SRC_DIR} # Install pip -RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py 'pip==20.3.3' +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ + python ${SRC_DIR}/get-pip.py # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels From 85e838038fbacb6fedba6a956e577f997cff67b1 Mon Sep 17 00:00:00 2001 From: Stefanie Taepke Date: Wed, 24 Mar 2021 17:42:50 +0100 Subject: [PATCH 093/213] Install rust as cryptography-requirement (#44) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * install rust as cryptography-requirement Installing cryptography fails with “This package requires Rust >=1.41.0.” if rust is not installed * install rust with cargo as cryptography-requirement --- examples/harvest/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index 49a345b..4afdbd7 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -16,7 +16,9 @@ RUN apk add --no-cache \ g++ \ libffi-dev \ openssl-dev \ - python3-dev + python3-dev \ + rust \ + cargo # Fetch and build the custom CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${HARVEST_GIT_URL}@${HARVEST_GIT_BRANCH}#egg=ckanext-harvest From 302448080e8714fa7c0bb21e25c40d94486307f3 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 24 Mar 2021 17:56:32 +0100 Subject: [PATCH 094/213] Pin pip to 20.3.3 and bump focal image version --- images/ckan/2.7/Dockerfile | 6 +++--- images/ckan/2.7/Dockerfile.focal | 12 ++++++------ images/ckan/2.8/Dockerfile | 6 +++--- images/ckan/2.8/Dockerfile.focal | 12 ++++++------ images/ckan/2.9/Dockerfile.focal | 6 +++--- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 3435bc3..3b042ad 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -47,7 +47,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan @@ -88,7 +88,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -130,7 +130,7 @@ RUN apk add --no-cache \ # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index d22a37f..6d55f10 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM ubuntu:focal-20201106 as ckanbuild +FROM ubuntu:focal-20210217 as ckanbuild # Used by Github Actions to tag the image with ENV IMAGE_TAG=2.7.10-focal @@ -72,7 +72,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan @@ -89,7 +89,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ########################### ### Default-Extensions #### ########################### -FROM ubuntu:focal-20201106 as extbuild +FROM ubuntu:focal-20210217 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -115,7 +115,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -123,7 +123,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM ubuntu:focal-20201106 +FROM ubuntu:focal-20210217 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan @@ -181,7 +181,7 @@ RUN apt-get update && \ # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 281995d..a7af7b4 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -43,7 +43,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan @@ -84,7 +84,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -123,7 +123,7 @@ RUN apk add --no-cache git \ # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index 37e5baa..adc8b18 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM ubuntu:focal-20201106 as ckanbuild +FROM ubuntu:focal-20210217 as ckanbuild # Used by Github Actions to tag the image with ENV IMAGE_TAG=2.8.7-focal @@ -64,7 +64,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build CKAN and requirements RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan @@ -81,7 +81,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ########################### ### Default-Extensions #### ########################### -FROM ubuntu:focal-20201106 as extbuild +FROM ubuntu:focal-20210217 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -107,7 +107,7 @@ RUN mkdir -p ${SRC_DIR} # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -115,7 +115,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM ubuntu:focal-20201106 +FROM ubuntu:focal-20210217 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan @@ -166,7 +166,7 @@ RUN apt-get update && \ # Install pip RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/pip/2.7/get-pip.py && \ - python ${SRC_DIR}/get-pip.py + python ${SRC_DIR}/get-pip.py 'pip==20.3.3' # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 2521306..278cae9 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM ubuntu:focal-20201106 as ckanbuild +FROM ubuntu:focal-20210217 as ckanbuild # Used by Github Actions to tag the image with ENV IMAGE_TAG=2.9.2-focal @@ -87,7 +87,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ########################### ### Default-Extensions #### ########################### -FROM ubuntu:focal-20201106 as extbuild +FROM ubuntu:focal-20210217 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -124,7 +124,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM ubuntu:focal-20201106 +FROM ubuntu:focal-20210217 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan From 3d957c35d7d5904e829b21bc523168b06cd4be06 Mon Sep 17 00:00:00 2001 From: gregoxx Date: Thu, 1 Apr 2021 09:45:24 +0100 Subject: [PATCH 095/213] add license and copyright noticesa --- NOTICE | 2 ++ .../api/3/action/status_show/index.html | 16 ++++++++++++++++ images/ckan/2.7/setup/app/maintenance/index.html | 16 ++++++++++++++++ images/ckan/2.7/setup/app/maintenance/serve.py | 16 ++++++++++++++++ images/ckan/2.7/setup/app/prerun.py | 16 ++++++++++++++++ .../api/3/action/status_show/index.html | 16 ++++++++++++++++ images/ckan/2.8/setup/app/maintenance/index.html | 16 ++++++++++++++++ images/ckan/2.8/setup/app/maintenance/serve.py | 16 ++++++++++++++++ images/ckan/2.8/setup/app/prerun.py | 16 ++++++++++++++++ .../api/3/action/status_show/index.html | 16 ++++++++++++++++ images/ckan/2.9/setup/app/maintenance/index.html | 16 ++++++++++++++++ images/ckan/2.9/setup/app/maintenance/serve.py | 16 ++++++++++++++++ images/ckan/2.9/setup/app/prerun.py | 16 ++++++++++++++++ images/ckan/2.9/setup/app/wsgi.py | 16 ++++++++++++++++ images/datapusher/setup/datapusher_settings.py | 16 ++++++++++++++++ images/datapusher/setup/wsgi.py | 16 ++++++++++++++++ 16 files changed, 242 insertions(+) create mode 100644 NOTICE diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..2ee6539 --- /dev/null +++ b/NOTICE @@ -0,0 +1,2 @@ +docker-ckan +Copyright (c) 2016 Keitaro AB diff --git a/images/ckan/2.7/setup/app/maintenance/api/3/action/status_show/index.html b/images/ckan/2.7/setup/app/maintenance/api/3/action/status_show/index.html index eb43256..50276ec 100644 --- a/images/ckan/2.7/setup/app/maintenance/api/3/action/status_show/index.html +++ b/images/ckan/2.7/setup/app/maintenance/api/3/action/status_show/index.html @@ -1,3 +1,19 @@ + + Maintenance diff --git a/images/ckan/2.7/setup/app/maintenance/index.html b/images/ckan/2.7/setup/app/maintenance/index.html index eb43256..50276ec 100644 --- a/images/ckan/2.7/setup/app/maintenance/index.html +++ b/images/ckan/2.7/setup/app/maintenance/index.html @@ -1,3 +1,19 @@ + + Maintenance diff --git a/images/ckan/2.7/setup/app/maintenance/serve.py b/images/ckan/2.7/setup/app/maintenance/serve.py index 23e2dde..96c0a15 100644 --- a/images/ckan/2.7/setup/app/maintenance/serve.py +++ b/images/ckan/2.7/setup/app/maintenance/serve.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2016 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + from SimpleHTTPServer import SimpleHTTPRequestHandler from BaseHTTPServer import HTTPServer from SocketServer import ThreadingMixIn diff --git a/images/ckan/2.7/setup/app/prerun.py b/images/ckan/2.7/setup/app/prerun.py index 5a19d3f..4065df5 100644 --- a/images/ckan/2.7/setup/app/prerun.py +++ b/images/ckan/2.7/setup/app/prerun.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2016 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + import os import sys import subprocess diff --git a/images/ckan/2.8/setup/app/maintenance/api/3/action/status_show/index.html b/images/ckan/2.8/setup/app/maintenance/api/3/action/status_show/index.html index eb43256..50276ec 100644 --- a/images/ckan/2.8/setup/app/maintenance/api/3/action/status_show/index.html +++ b/images/ckan/2.8/setup/app/maintenance/api/3/action/status_show/index.html @@ -1,3 +1,19 @@ + + Maintenance diff --git a/images/ckan/2.8/setup/app/maintenance/index.html b/images/ckan/2.8/setup/app/maintenance/index.html index eb43256..50276ec 100644 --- a/images/ckan/2.8/setup/app/maintenance/index.html +++ b/images/ckan/2.8/setup/app/maintenance/index.html @@ -1,3 +1,19 @@ + + Maintenance diff --git a/images/ckan/2.8/setup/app/maintenance/serve.py b/images/ckan/2.8/setup/app/maintenance/serve.py index 23e2dde..96c0a15 100644 --- a/images/ckan/2.8/setup/app/maintenance/serve.py +++ b/images/ckan/2.8/setup/app/maintenance/serve.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2016 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + from SimpleHTTPServer import SimpleHTTPRequestHandler from BaseHTTPServer import HTTPServer from SocketServer import ThreadingMixIn diff --git a/images/ckan/2.8/setup/app/prerun.py b/images/ckan/2.8/setup/app/prerun.py index 5a19d3f..4065df5 100644 --- a/images/ckan/2.8/setup/app/prerun.py +++ b/images/ckan/2.8/setup/app/prerun.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2016 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + import os import sys import subprocess diff --git a/images/ckan/2.9/setup/app/maintenance/api/3/action/status_show/index.html b/images/ckan/2.9/setup/app/maintenance/api/3/action/status_show/index.html index eb43256..50276ec 100644 --- a/images/ckan/2.9/setup/app/maintenance/api/3/action/status_show/index.html +++ b/images/ckan/2.9/setup/app/maintenance/api/3/action/status_show/index.html @@ -1,3 +1,19 @@ + + Maintenance diff --git a/images/ckan/2.9/setup/app/maintenance/index.html b/images/ckan/2.9/setup/app/maintenance/index.html index eb43256..50276ec 100644 --- a/images/ckan/2.9/setup/app/maintenance/index.html +++ b/images/ckan/2.9/setup/app/maintenance/index.html @@ -1,3 +1,19 @@ + + Maintenance diff --git a/images/ckan/2.9/setup/app/maintenance/serve.py b/images/ckan/2.9/setup/app/maintenance/serve.py index fa56d40..77f3bd4 100644 --- a/images/ckan/2.9/setup/app/maintenance/serve.py +++ b/images/ckan/2.9/setup/app/maintenance/serve.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2016 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler import os diff --git a/images/ckan/2.9/setup/app/prerun.py b/images/ckan/2.9/setup/app/prerun.py index ca4598f..f31c719 100644 --- a/images/ckan/2.9/setup/app/prerun.py +++ b/images/ckan/2.9/setup/app/prerun.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2016 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + import os import sys import subprocess diff --git a/images/ckan/2.9/setup/app/wsgi.py b/images/ckan/2.9/setup/app/wsgi.py index 2ad03a7..63f3a7c 100644 --- a/images/ckan/2.9/setup/app/wsgi.py +++ b/images/ckan/2.9/setup/app/wsgi.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2016 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + # -*- coding: utf-8 -*- import os diff --git a/images/datapusher/setup/datapusher_settings.py b/images/datapusher/setup/datapusher_settings.py index 8a4413a..4bc28af 100644 --- a/images/datapusher/setup/datapusher_settings.py +++ b/images/datapusher/setup/datapusher_settings.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2016 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + import uuid import os diff --git a/images/datapusher/setup/wsgi.py b/images/datapusher/setup/wsgi.py index 8701ffa..5ce7c4b 100644 --- a/images/datapusher/setup/wsgi.py +++ b/images/datapusher/setup/wsgi.py @@ -1,3 +1,19 @@ +""" +Copyright (c) 2016 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + import os import sys From c51d5eb6bf216f9aaf2c2ef423172b92e11e4639 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Thu, 6 May 2021 05:52:29 +0200 Subject: [PATCH 096/213] Add PGDATA env var to db service in compose --- compose/docker-compose.yml | 1 + examples/harvest/docker-compose.yml | 1 + examples/s3filestore/docker-compose.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index c5c17b6..3ea6a4d 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -58,6 +58,7 @@ services: args: - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - PGDATA=/var/lib/postgresql/data/db networks: - backend environment: diff --git a/examples/harvest/docker-compose.yml b/examples/harvest/docker-compose.yml index 7ad2d70..775e75c 100644 --- a/examples/harvest/docker-compose.yml +++ b/examples/harvest/docker-compose.yml @@ -156,6 +156,7 @@ services: args: - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - PGDATA=/var/lib/postgresql/data/db networks: - backend environment: diff --git a/examples/s3filestore/docker-compose.yml b/examples/s3filestore/docker-compose.yml index 45d8359..d3bdb4c 100644 --- a/examples/s3filestore/docker-compose.yml +++ b/examples/s3filestore/docker-compose.yml @@ -61,6 +61,7 @@ services: environment: - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - PGDATA=/var/lib/postgresql/data/db volumes: - pg_data:/var/lib/postgresql/data healthcheck: From 896de5a40fd2209e9624f7c30a943b26853460d3 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Fri, 7 May 2021 06:03:06 +0200 Subject: [PATCH 097/213] Move PGDATA env var to environment section --- compose/docker-compose.yml | 2 +- examples/harvest/docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index 3ea6a4d..5711653 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -58,12 +58,12 @@ services: args: - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - - PGDATA=/var/lib/postgresql/data/db networks: - backend environment: - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - PGDATA=/var/lib/postgresql/data/db volumes: - pg_data:/var/lib/postgresql/data healthcheck: diff --git a/examples/harvest/docker-compose.yml b/examples/harvest/docker-compose.yml index 775e75c..f0f2d5f 100644 --- a/examples/harvest/docker-compose.yml +++ b/examples/harvest/docker-compose.yml @@ -156,12 +156,12 @@ services: args: - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - - PGDATA=/var/lib/postgresql/data/db networks: - backend environment: - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - PGDATA=/var/lib/postgresql/data/db volumes: - pg_data:/var/lib/postgresql/data healthcheck: From a9090c97931406189f9db17286847940dd8c8f89 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 19 May 2021 13:01:46 +0200 Subject: [PATCH 098/213] CKAN patch releases 2.7.11 2.8.8 2.9.3 (#50) * CKAN patch releases 2.7.11 2.8.8 2.9.3 * Update solrconfig filenames * Update harvest example default CKAN version to 2.9.3 * Fix symbolic link --- Readme.md | 8 ++++---- compose/.env | 2 +- compose/solr/solrconfig-2.7.10.xml | 1 - compose/solr/solrconfig-2.7.11.xml | 1 + .../{solrconfig-2.8.7.xml => solrconfig-2.8.8.xml} | 0 .../{solrconfig-2.9.2.xml => solrconfig-2.9.3.xml} | 0 examples/harvest/.env | 2 +- examples/harvest/Dockerfile | 4 ++-- examples/s3filestore/.env | 2 +- examples/s3filestore/Dockerfile | 4 ++-- images/ckan/2.7/Dockerfile | 4 ++-- images/ckan/2.7/Dockerfile.focal | 10 +++++----- images/ckan/2.8/Dockerfile | 4 ++-- images/ckan/2.8/Dockerfile.focal | 10 +++++----- images/ckan/2.9/Dockerfile | 4 ++-- images/ckan/2.9/Dockerfile.focal | 10 +++++----- 16 files changed, 33 insertions(+), 33 deletions(-) delete mode 120000 compose/solr/solrconfig-2.7.10.xml create mode 120000 compose/solr/solrconfig-2.7.11.xml rename compose/solr/{solrconfig-2.8.7.xml => solrconfig-2.8.8.xml} (100%) rename compose/solr/{solrconfig-2.9.2.xml => solrconfig-2.9.3.xml} (100%) diff --git a/Readme.md b/Readme.md index 15318d8..ebc9369 100644 --- a/Readme.md +++ b/Readme.md @@ -48,7 +48,7 @@ We recommend to use a multi-stage approach to extend the docker images that we p ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.2 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.3 as extbuild # Switch to the root user USER root @@ -62,7 +62,7 @@ RUN pip wheel --wheel-dir=/wheels git+https://github.com/acmecorp/ckanext-acme@0 ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.2 +FROM ghcr.io/keitaroinc/ckan:2.9.3 # Add the custom extensions to the plugins list ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher acme @@ -92,9 +92,9 @@ You can add scripts to CKAN custom images and copy them to the *docker-afterinit ## Build To build a CKAN image run: ```sh -docker build --tag ghcr.io/keitaroinc/ckan:2.9.2 images/ckan/2.9 +docker build --tag ghcr.io/keitaroinc/ckan:2.9.3 images/ckan/2.9 ``` -The –-tag ghcr.io/keitaroinc/ckan:2.9.2 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.2 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. +The –-tag ghcr.io/keitaroinc/ckan:2.9.3 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.3 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. ## Upload to DockerHub >*It's recommended to upload built images to DockerHub* diff --git a/compose/.env b/compose/.env index 89a9c51..446d552 100644 --- a/compose/.env +++ b/compose/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.2 +CKAN_VERSION=2.9.3 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/solr/solrconfig-2.7.10.xml b/compose/solr/solrconfig-2.7.10.xml deleted file mode 120000 index 207d108..0000000 --- a/compose/solr/solrconfig-2.7.10.xml +++ /dev/null @@ -1 +0,0 @@ -solrconfig-2.8.7.xml \ No newline at end of file diff --git a/compose/solr/solrconfig-2.7.11.xml b/compose/solr/solrconfig-2.7.11.xml new file mode 120000 index 0000000..0f440c8 --- /dev/null +++ b/compose/solr/solrconfig-2.7.11.xml @@ -0,0 +1 @@ +solrconfig-2.8.8.xml \ No newline at end of file diff --git a/compose/solr/solrconfig-2.8.7.xml b/compose/solr/solrconfig-2.8.8.xml similarity index 100% rename from compose/solr/solrconfig-2.8.7.xml rename to compose/solr/solrconfig-2.8.8.xml diff --git a/compose/solr/solrconfig-2.9.2.xml b/compose/solr/solrconfig-2.9.3.xml similarity index 100% rename from compose/solr/solrconfig-2.9.2.xml rename to compose/solr/solrconfig-2.9.3.xml diff --git a/examples/harvest/.env b/examples/harvest/.env index 514724c..446d552 100644 --- a/examples/harvest/.env +++ b/examples/harvest/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.1 +CKAN_VERSION=2.9.3 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index 55c0d41..9dc972c 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.2 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.3 as extbuild # Locations and tags, please use specific tags or revisions ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest @@ -30,7 +30,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.2 +FROM ghcr.io/keitaroinc/ckan:2.9.3 LABEL maintainer="Keitaro Inc " diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env index d519cb2..a6a2833 100644 --- a/examples/s3filestore/.env +++ b/examples/s3filestore/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.8.7 +CKAN_VERSION=2.8.8 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index 8f58d14..9470e72 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.8.7 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.8.8 as extbuild # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore @@ -20,7 +20,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.8.7 +FROM ghcr.io/keitaroinc/ckan:2.8.8 LABEL maintainer="Keitaro Inc " diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 3b042ad..df2808b 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.13 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.7.10 +ENV IMAGE_TAG=2.7.11 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.7.10 +ENV GIT_BRANCH=ckan-2.7.11 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index 6d55f10..4d64afb 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -1,14 +1,14 @@ ################## ### Build CKAN ### ################## -FROM ubuntu:focal-20210217 as ckanbuild +FROM ubuntu:focal-20210416 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.7.10-focal +ENV IMAGE_TAG=2.7.11-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.7.10 +ENV GIT_BRANCH=ckan-2.7.11 # Set timezone ENV TZ=UTC @@ -89,7 +89,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ########################### ### Default-Extensions #### ########################### -FROM ubuntu:focal-20210217 as extbuild +FROM ubuntu:focal-20210416 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -123,7 +123,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM ubuntu:focal-20210217 +FROM ubuntu:focal-20210416 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index a7af7b4..ded72f4 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.13 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.8.7 +ENV IMAGE_TAG=2.8.8 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.7 +ENV GIT_BRANCH=ckan-2.8.8 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index adc8b18..c89f38f 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -1,14 +1,14 @@ ################## ### Build CKAN ### ################## -FROM ubuntu:focal-20210217 as ckanbuild +FROM ubuntu:focal-20210416 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.8.7-focal +ENV IMAGE_TAG=2.8.8-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.7 +ENV GIT_BRANCH=ckan-2.8.8 # Set timezone ENV TZ=UTC @@ -81,7 +81,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ########################### ### Default-Extensions #### ########################### -FROM ubuntu:focal-20210217 as extbuild +FROM ubuntu:focal-20210416 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -115,7 +115,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM ubuntu:focal-20210217 +FROM ubuntu:focal-20210416 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 0baa7c5..1080e3b 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.13 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.2 +ENV IMAGE_TAG=2.9.3 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.2 +ENV GIT_BRANCH=ckan-2.9.3 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 278cae9..39913fb 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -1,14 +1,14 @@ ################## ### Build CKAN ### ################## -FROM ubuntu:focal-20210217 as ckanbuild +FROM ubuntu:focal-20210416 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.2-focal +ENV IMAGE_TAG=2.9.3-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.2 +ENV GIT_BRANCH=ckan-2.9.3 # Set timezone ENV TZ=UTC @@ -87,7 +87,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ########################### ### Default-Extensions #### ########################### -FROM ubuntu:focal-20210217 as extbuild +FROM ubuntu:focal-20210416 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -124,7 +124,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM ubuntu:focal-20210217 +FROM ubuntu:focal-20210416 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan From 165419d767de4a8fe7703e84f03776e5f0701f8f Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 19 May 2021 13:31:52 +0200 Subject: [PATCH 099/213] Update greenlet and gevent versions, pin alpine version to specific minor revision --- images/ckan/2.7/Dockerfile | 10 +++++----- images/ckan/2.7/Dockerfile.focal | 4 ++-- images/ckan/2.8/Dockerfile | 10 +++++----- images/ckan/2.8/Dockerfile.focal | 4 ++-- images/ckan/2.9/Dockerfile | 10 +++++----- images/ckan/2.9/Dockerfile.focal | 4 ++-- images/datapusher/Dockerfile | 4 ++-- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index df2808b..5a39a7d 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.13 as ckanbuild +FROM alpine:3.13.5 as ckanbuild # Used by Github Actions to tag the image with ENV IMAGE_TAG=2.7.11 @@ -58,13 +58,13 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.13 as extbuild +FROM alpine:3.13.5 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -96,7 +96,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.13 +FROM alpine:3.13.5 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan @@ -138,7 +138,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.1.2 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index 4d64afb..a5fa0a1 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -83,7 +83,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### @@ -189,7 +189,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.1.2 # Create a local user and group to run the app RUN groupadd -g 92 ckan && \ diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index ded72f4..6fb652d 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.13 as ckanbuild +FROM alpine:3.13.5 as ckanbuild # Used by Github Actions to tag the image with ENV IMAGE_TAG=2.8.8 @@ -54,13 +54,13 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.13 as extbuild +FROM alpine:3.13.5 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -92,7 +92,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.13 +FROM alpine:3.13.5 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan @@ -131,7 +131,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.1.2 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index c89f38f..52dbac3 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -75,7 +75,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### @@ -174,7 +174,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.1.2 # Create a local user and group to run the app RUN groupadd -g 92 ckan && \ diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 1080e3b..70e7cd1 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.13 as ckanbuild +FROM alpine:3.13.5 as ckanbuild # Used by Github Actions to tag the image with ENV IMAGE_TAG=2.9.3 @@ -60,13 +60,13 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.13 as extbuild +FROM alpine:3.13.5 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -101,7 +101,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.13 +FROM alpine:3.13.5 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan @@ -142,7 +142,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.1.2 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 39913fb..15efe51 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -81,7 +81,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### @@ -186,7 +186,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.1.2 # Create a local user and group to run the app RUN groupadd -g 92 ckan && \ diff --git a/images/datapusher/Dockerfile b/images/datapusher/Dockerfile index 4894864..fcb51b8 100644 --- a/images/datapusher/Dockerfile +++ b/images/datapusher/Dockerfile @@ -1,7 +1,7 @@ ############# ### Build ### ############# -FROM alpine:3.13 as build +FROM alpine:3.13.5 as build # Used by Github Actions to tag the image with ENV IMAGE_TAG=0.0.17 @@ -56,7 +56,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ############ ### MAIN ### ############ -FROM alpine:3.13 +FROM alpine:3.13.5 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan From fa62c30b11b66ea27a648be4334a0cf6ab9128df Mon Sep 17 00:00:00 2001 From: Blagoja Stojkoski Date: Thu, 20 May 2021 11:34:55 +0200 Subject: [PATCH 100/213] updating s3filestore example to use CKAN 2.9 (#52) --- examples/s3filestore/.ckan-env | 3 ++- examples/s3filestore/Dockerfile | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/s3filestore/.ckan-env b/examples/s3filestore/.ckan-env index 95e1820..b862517 100644 --- a/examples/s3filestore/.ckan-env +++ b/examples/s3filestore/.ckan-env @@ -8,7 +8,7 @@ CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 CKAN_MAX_UPLOAD_SIZE_MB=10 # CKAN Plugins -CKAN__PLUGINS=envvars s3filestore image_view text_view recline_view datastore datapusher +CKAN__PLUGINS=envvars s3filestore image_view webpage_view text_view recline_view datastore datapusher # CKAN requires storage path to be set in order for filestore to be enabled CKAN__STORAGE_PATH=/srv/app/data CKAN__WEBASSETS__PATH=/srv/app/data/webassets @@ -16,6 +16,7 @@ CKAN__WEBASSETS__PATH=/srv/app/data/webassets CKAN_SYSADMIN_NAME=sysadmin CKAN_SYSADMIN_PASSWORD=password CKAN_SYSADMIN_EMAIL=sysadmin@ckantest.com +CKAN__VIEWS__DEFAULT_VIEWS=image_view webpage_view text_view recline_view # Email settings CKAN_SMTP_SERVER=smtp.corporateict.domain:25 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index 9470e72..8585521 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.8.8 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.3 as extbuild # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore @@ -20,11 +20,11 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.8.8 +FROM ghcr.io/keitaroinc/ckan:2.9.3 LABEL maintainer="Keitaro Inc " -ENV CKAN__PLUGINS envvars s3filestore image_view text_view recline_view datastore datapusher +ENV CKAN__PLUGINS envvars s3filestore image_view webpage_view text_view recline_view datastore datapusher # Switch to the root user USER root @@ -34,7 +34,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels # Install and enable the custom extensions RUN pip install --no-index --find-links=/srv/app/ext_wheels ckanext-s3filestore && \ pip install --no-index --find-links=/srv/app/ext_wheels -r /srv/app/ext_wheels/s3filestore.txt && \ - paster --plugin=ckan config-tool "${APP_DIR}/production.ini" "ckan.plugins = ${CKAN__PLUGINS}" && \ + ckan config-tool "${APP_DIR}/production.ini" "ckan.plugins = ${CKAN__PLUGINS}" && \ chown -R ckan:ckan /srv/app # Remove wheels From 6b9c96ceb998e58a20a1c65489a5907254cf47e4 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Thu, 16 Sep 2021 15:19:37 +0200 Subject: [PATCH 101/213] New CKAN patch releases 2.9.4, 2.8.9, 2.7.12 --- Readme.md | 8 ++++---- compose/.env | 2 +- compose/solr/solrconfig-2.7.11.xml | 1 - compose/solr/solrconfig-2.7.12.xml | 1 + .../{solrconfig-2.8.8.xml => solrconfig-2.8.9.xml} | 0 .../{solrconfig-2.9.3.xml => solrconfig-2.9.4.xml} | 0 examples/harvest/.env | 2 +- examples/harvest/Dockerfile | 4 ++-- examples/s3filestore/.env | 2 +- examples/s3filestore/Dockerfile | 4 ++-- images/ckan/2.7/Dockerfile | 10 +++++----- images/ckan/2.7/Dockerfile.focal | 10 +++++----- images/ckan/2.8/Dockerfile | 10 +++++----- images/ckan/2.8/Dockerfile.focal | 10 +++++----- images/ckan/2.9/Dockerfile | 10 +++++----- images/ckan/2.9/Dockerfile.focal | 10 +++++----- images/datapusher/Dockerfile | 4 ++-- 17 files changed, 44 insertions(+), 44 deletions(-) delete mode 120000 compose/solr/solrconfig-2.7.11.xml create mode 120000 compose/solr/solrconfig-2.7.12.xml rename compose/solr/{solrconfig-2.8.8.xml => solrconfig-2.8.9.xml} (100%) rename compose/solr/{solrconfig-2.9.3.xml => solrconfig-2.9.4.xml} (100%) diff --git a/Readme.md b/Readme.md index ebc9369..ffd25bc 100644 --- a/Readme.md +++ b/Readme.md @@ -48,7 +48,7 @@ We recommend to use a multi-stage approach to extend the docker images that we p ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.3 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.4 as extbuild # Switch to the root user USER root @@ -62,7 +62,7 @@ RUN pip wheel --wheel-dir=/wheels git+https://github.com/acmecorp/ckanext-acme@0 ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.3 +FROM ghcr.io/keitaroinc/ckan:2.9.4 # Add the custom extensions to the plugins list ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher acme @@ -92,9 +92,9 @@ You can add scripts to CKAN custom images and copy them to the *docker-afterinit ## Build To build a CKAN image run: ```sh -docker build --tag ghcr.io/keitaroinc/ckan:2.9.3 images/ckan/2.9 +docker build --tag ghcr.io/keitaroinc/ckan:2.9.4 images/ckan/2.9 ``` -The –-tag ghcr.io/keitaroinc/ckan:2.9.3 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.3 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. +The –-tag ghcr.io/keitaroinc/ckan:2.9.4 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.4 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. ## Upload to DockerHub >*It's recommended to upload built images to DockerHub* diff --git a/compose/.env b/compose/.env index 446d552..57ae85f 100644 --- a/compose/.env +++ b/compose/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.3 +CKAN_VERSION=2.9.4 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/solr/solrconfig-2.7.11.xml b/compose/solr/solrconfig-2.7.11.xml deleted file mode 120000 index 0f440c8..0000000 --- a/compose/solr/solrconfig-2.7.11.xml +++ /dev/null @@ -1 +0,0 @@ -solrconfig-2.8.8.xml \ No newline at end of file diff --git a/compose/solr/solrconfig-2.7.12.xml b/compose/solr/solrconfig-2.7.12.xml new file mode 120000 index 0000000..6269124 --- /dev/null +++ b/compose/solr/solrconfig-2.7.12.xml @@ -0,0 +1 @@ +solrconfig-2.8.9.xml \ No newline at end of file diff --git a/compose/solr/solrconfig-2.8.8.xml b/compose/solr/solrconfig-2.8.9.xml similarity index 100% rename from compose/solr/solrconfig-2.8.8.xml rename to compose/solr/solrconfig-2.8.9.xml diff --git a/compose/solr/solrconfig-2.9.3.xml b/compose/solr/solrconfig-2.9.4.xml similarity index 100% rename from compose/solr/solrconfig-2.9.3.xml rename to compose/solr/solrconfig-2.9.4.xml diff --git a/examples/harvest/.env b/examples/harvest/.env index 446d552..57ae85f 100644 --- a/examples/harvest/.env +++ b/examples/harvest/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.3 +CKAN_VERSION=2.9.4 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index 9dc972c..e708d6a 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.3 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.4 as extbuild # Locations and tags, please use specific tags or revisions ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest @@ -30,7 +30,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.3 +FROM ghcr.io/keitaroinc/ckan:2.9.4 LABEL maintainer="Keitaro Inc " diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env index a6a2833..57ae85f 100644 --- a/examples/s3filestore/.env +++ b/examples/s3filestore/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.8.8 +CKAN_VERSION=2.9.4 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index 8585521..0d55cee 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.3 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.4 as extbuild # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore @@ -20,7 +20,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.3 +FROM ghcr.io/keitaroinc/ckan:2.9.4 LABEL maintainer="Keitaro Inc " diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 5a39a7d..c9b1817 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -1,14 +1,14 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.13.5 as ckanbuild +FROM alpine:3.14.2 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.7.11 +ENV IMAGE_TAG=2.7.12 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.7.11 +ENV GIT_BRANCH=ckan-2.7.12 # Set src dirs ENV SRC_DIR=/srv/app/src @@ -64,7 +64,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.13.5 as extbuild +FROM alpine:3.14.2 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -96,7 +96,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.13.5 +FROM alpine:3.14.2 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index a5fa0a1..bc5c9e2 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -1,14 +1,14 @@ ################## ### Build CKAN ### ################## -FROM ubuntu:focal-20210416 as ckanbuild +FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.7.11-focal +ENV IMAGE_TAG=2.7.12-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.7.11 +ENV GIT_BRANCH=ckan-2.7.12 # Set timezone ENV TZ=UTC @@ -89,7 +89,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### ### Default-Extensions #### ########################### -FROM ubuntu:focal-20210416 as extbuild +FROM ubuntu:focal-20210827 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -123,7 +123,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM ubuntu:focal-20210416 +FROM ubuntu:focal-20210827 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 6fb652d..354e8d2 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -1,14 +1,14 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.13.5 as ckanbuild +FROM alpine:3.14.2 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.8.8 +ENV IMAGE_TAG=2.8.9 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.8 +ENV GIT_BRANCH=ckan-2.8.9 # Set src dirs ENV SRC_DIR=/srv/app/src @@ -60,7 +60,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.13.5 as extbuild +FROM alpine:3.14.2 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -92,7 +92,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.13.5 +FROM alpine:3.14.2 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index 52dbac3..199f28b 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -1,14 +1,14 @@ ################## ### Build CKAN ### ################## -FROM ubuntu:focal-20210416 as ckanbuild +FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.8.8-focal +ENV IMAGE_TAG=2.8.9-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.8 +ENV GIT_BRANCH=ckan-2.8.9 # Set timezone ENV TZ=UTC @@ -81,7 +81,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### ### Default-Extensions #### ########################### -FROM ubuntu:focal-20210416 as extbuild +FROM ubuntu:focal-20210827 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -115,7 +115,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM ubuntu:focal-20210416 +FROM ubuntu:focal-20210827 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 70e7cd1..121b909 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -1,14 +1,14 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.13.5 as ckanbuild +FROM alpine:3.14.2 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.3 +ENV IMAGE_TAG=2.9.4 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.3 +ENV GIT_BRANCH=ckan-2.9.4 # Set src dirs ENV SRC_DIR=/srv/app/src @@ -66,7 +66,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.13.5 as extbuild +FROM alpine:3.14.2 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -101,7 +101,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.13.5 +FROM alpine:3.14.2 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 15efe51..f38dd17 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -1,14 +1,14 @@ ################## ### Build CKAN ### ################## -FROM ubuntu:focal-20210416 as ckanbuild +FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.3-focal +ENV IMAGE_TAG=2.9.4-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.3 +ENV GIT_BRANCH=ckan-2.9.4 # Set timezone ENV TZ=UTC @@ -87,7 +87,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### ### Default-Extensions #### ########################### -FROM ubuntu:focal-20210416 as extbuild +FROM ubuntu:focal-20210827 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -124,7 +124,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM ubuntu:focal-20210416 +FROM ubuntu:focal-20210827 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan diff --git a/images/datapusher/Dockerfile b/images/datapusher/Dockerfile index fcb51b8..352bf95 100644 --- a/images/datapusher/Dockerfile +++ b/images/datapusher/Dockerfile @@ -1,7 +1,7 @@ ############# ### Build ### ############# -FROM alpine:3.13.5 as build +FROM alpine:3.14.2 as build # Used by Github Actions to tag the image with ENV IMAGE_TAG=0.0.17 @@ -56,7 +56,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ############ ### MAIN ### ############ -FROM alpine:3.13.5 +FROM alpine:3.14.2 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan From 4ebef5c6d9f630a3a9815ef46cabd7a4b05cbc74 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 22 Sep 2021 17:11:27 +0200 Subject: [PATCH 102/213] Pin python version to python 3.8 --- images/ckan/2.9/Dockerfile | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 121b909..2d71771 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -17,10 +17,13 @@ ENV PIP_SRC=${SRC_DIR} WORKDIR ${SRC_DIR} # Packages to build CKAN requirements and plugins -RUN apk add --no-cache \ +# Make sure we install python 3.8, cause CKAN is not compatible with 3.9 +RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.13/main \ + python3=3.8.10-r0 \ + python3-dev=3.8.10-r0 && \ + apk add --no-cache \ git \ curl \ - python3 \ postgresql-dev \ linux-headers \ gcc \ @@ -33,7 +36,6 @@ RUN apk add --no-cache \ musl-dev \ pcre-dev \ pcre \ - python3-dev \ libffi-dev \ libxml2-dev \ libxslt-dev @@ -79,11 +81,13 @@ ENV DEFAULT_EXTENSIONS envvars ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars ENV ENVVARS_GIT_BRANCH=0.0.1 -RUN apk add --no-cache \ +# Make sure we install python 3.8, cause CKAN is not compatible with 3.9 +RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.13/main \ + python3=3.8.10-r0 \ + python3-dev=3.8.10-r0 && \ + apk add --no-cache \ git \ - curl \ - python3 \ - python3-dev + curl # Link python to python3 RUN ln -s /usr/bin/python3 /usr/bin/python @@ -115,12 +119,14 @@ ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher # Install necessary packages to run CKAN -RUN apk add --no-cache git \ +# Make sure we install python 3.8, cause CKAN is not compatible with 3.9 +RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.13/main \ + python3=3.8.10-r0 && \ + apk add --no-cache git \ bash \ gettext \ curl \ postgresql-client \ - python3 \ libmagic \ pcre \ libxslt \ From 02870cdb99fb0fb2780b7c5b832ef7a3348eb196 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Wed, 22 Sep 2021 18:16:30 +0200 Subject: [PATCH 103/213] Pin python version to python 3.8 in examples and readme --- Readme.md | 4 +++- examples/harvest/Dockerfile | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index ffd25bc..3c2fddb 100644 --- a/Readme.md +++ b/Readme.md @@ -54,7 +54,9 @@ FROM ghcr.io/keitaroinc/ckan:2.9.4 as extbuild USER root # Install any system packages necessary to build extensions -RUN apk add --no-cache python3-dev +# Make sure we install python 3.8, cause CKAN is not compatible with 3.9 +RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.13/main \ + python3-dev=3.8.10-r0 # Fetch and build the custom CKAN extensions RUN pip wheel --wheel-dir=/wheels git+https://github.com/acmecorp/ckanext-acme@0.0.1#egg=ckanext-acme diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index e708d6a..edc747a 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -11,12 +11,14 @@ ENV HARVEST_GIT_BRANCH=v1.3.1 USER root # Install necessary packages to build extensions -RUN apk add --no-cache \ +# Make sure we install python 3.8, cause CKAN is not compatible with 3.9 +RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.13/main \ + python3-dev=3.8.10-r0 && \ + apk add --no-cache \ gcc \ g++ \ libffi-dev \ openssl-dev \ - python3-dev \ rust \ cargo From 46ab021d9da8851d054d05b6497e2bb420914391 Mon Sep 17 00:00:00 2001 From: Sam Mueller <83286605+Locutus66@users.noreply.github.com> Date: Sun, 5 Dec 2021 19:09:00 -0500 Subject: [PATCH 104/213] check_solr_connection bug fix If the ResponseHeader contains "zkConnected": false, your deployment will throw a "NameError: name 'false' is not defined" without this proposed line I added. --- images/ckan/2.9/setup/app/prerun.py | 1 + 1 file changed, 1 insertion(+) diff --git a/images/ckan/2.9/setup/app/prerun.py b/images/ckan/2.9/setup/app/prerun.py index f31c719..2dc1301 100644 --- a/images/ckan/2.9/setup/app/prerun.py +++ b/images/ckan/2.9/setup/app/prerun.py @@ -83,6 +83,7 @@ def check_solr_connection(retry=None): conn_info = connection.read() # SolrCloud conn_info = re.sub(r'"zkConnected":true', '"zkConnected":True', conn_info.decode('utf-8')) + conn_info = re.sub(r'"zkConnected":false', '"zkConnected":False', conn_info.decode('utf-8')) eval(conn_info) def init_db(): From bf3a02993735b29b65b5b84751a69803af2fd9db Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Mon, 17 Jan 2022 19:31:00 +0100 Subject: [PATCH 105/213] CKAN patch releases 2.8.10 2.9.5 --- Readme.md | 8 ++++---- compose/.env | 2 +- .../solr/{solrconfig-2.8.9.xml => solrconfig-2.8.10.xml} | 0 .../solr/{solrconfig-2.9.4.xml => solrconfig-2.9.5.xml} | 0 examples/harvest/.env | 2 +- examples/harvest/Dockerfile | 4 ++-- examples/s3filestore/.env | 2 +- examples/s3filestore/Dockerfile | 4 ++-- images/ckan/2.8/Dockerfile | 4 ++-- images/ckan/2.8/Dockerfile.focal | 4 ++-- images/ckan/2.9/Dockerfile | 4 ++-- images/ckan/2.9/Dockerfile.focal | 4 ++-- 12 files changed, 19 insertions(+), 19 deletions(-) rename compose/solr/{solrconfig-2.8.9.xml => solrconfig-2.8.10.xml} (100%) rename compose/solr/{solrconfig-2.9.4.xml => solrconfig-2.9.5.xml} (100%) diff --git a/Readme.md b/Readme.md index 3c2fddb..7c5887d 100644 --- a/Readme.md +++ b/Readme.md @@ -48,7 +48,7 @@ We recommend to use a multi-stage approach to extend the docker images that we p ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.4 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.5 as extbuild # Switch to the root user USER root @@ -64,7 +64,7 @@ RUN pip wheel --wheel-dir=/wheels git+https://github.com/acmecorp/ckanext-acme@0 ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.4 +FROM ghcr.io/keitaroinc/ckan:2.9.5 # Add the custom extensions to the plugins list ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher acme @@ -94,9 +94,9 @@ You can add scripts to CKAN custom images and copy them to the *docker-afterinit ## Build To build a CKAN image run: ```sh -docker build --tag ghcr.io/keitaroinc/ckan:2.9.4 images/ckan/2.9 +docker build --tag ghcr.io/keitaroinc/ckan:2.9.5 images/ckan/2.9 ``` -The –-tag ghcr.io/keitaroinc/ckan:2.9.4 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.4 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. +The –-tag ghcr.io/keitaroinc/ckan:2.9.5 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.5 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. ## Upload to DockerHub >*It's recommended to upload built images to DockerHub* diff --git a/compose/.env b/compose/.env index 57ae85f..8eb7389 100644 --- a/compose/.env +++ b/compose/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.4 +CKAN_VERSION=2.9.5 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/solr/solrconfig-2.8.9.xml b/compose/solr/solrconfig-2.8.10.xml similarity index 100% rename from compose/solr/solrconfig-2.8.9.xml rename to compose/solr/solrconfig-2.8.10.xml diff --git a/compose/solr/solrconfig-2.9.4.xml b/compose/solr/solrconfig-2.9.5.xml similarity index 100% rename from compose/solr/solrconfig-2.9.4.xml rename to compose/solr/solrconfig-2.9.5.xml diff --git a/examples/harvest/.env b/examples/harvest/.env index 57ae85f..8eb7389 100644 --- a/examples/harvest/.env +++ b/examples/harvest/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.4 +CKAN_VERSION=2.9.5 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index edc747a..0975804 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.4 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.5 as extbuild # Locations and tags, please use specific tags or revisions ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest @@ -32,7 +32,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.4 +FROM ghcr.io/keitaroinc/ckan:2.9.5 LABEL maintainer="Keitaro Inc " diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env index 57ae85f..8eb7389 100644 --- a/examples/s3filestore/.env +++ b/examples/s3filestore/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.4 +CKAN_VERSION=2.9.5 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index 0d55cee..c8e6a7e 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.4 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.5 as extbuild # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore @@ -20,7 +20,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.4 +FROM ghcr.io/keitaroinc/ckan:2.9.5 LABEL maintainer="Keitaro Inc " diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 354e8d2..fc47399 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.14.2 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.8.9 +ENV IMAGE_TAG=2.8.10 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.9 +ENV GIT_BRANCH=ckan-2.8.10 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index 199f28b..6ff0a95 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.8.9-focal +ENV IMAGE_TAG=2.8.10-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.9 +ENV GIT_BRANCH=ckan-2.8.10 # Set timezone ENV TZ=UTC diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 2d71771..cf14a8f 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.14.2 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.4 +ENV IMAGE_TAG=2.9.5 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.4 +ENV GIT_BRANCH=ckan-2.9.5 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index f38dd17..87cd417 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.4-focal +ENV IMAGE_TAG=2.9.5-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.4 +ENV GIT_BRANCH=ckan-2.9.5 # Set timezone ENV TZ=UTC From a9abd4b36b7efb610fdcc33e63ee81327659a31c Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Thu, 20 Jan 2022 15:21:59 +0100 Subject: [PATCH 106/213] * reverts to alpine 3.13 and native python 3.8 package * downgrades setuptools to 44.1.0 to fix ckan install issues on Ubuntu * fixes solrconfig xml for CKAN 2.7 --- compose/solr/solrconfig-2.7.12.xml | 2 +- images/ckan/2.9/Dockerfile | 28 +++++++++++----------------- images/ckan/2.9/Dockerfile.focal | 6 ++++++ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/compose/solr/solrconfig-2.7.12.xml b/compose/solr/solrconfig-2.7.12.xml index 6269124..6e325cc 120000 --- a/compose/solr/solrconfig-2.7.12.xml +++ b/compose/solr/solrconfig-2.7.12.xml @@ -1 +1 @@ -solrconfig-2.8.9.xml \ No newline at end of file +solrconfig-2.8.10.xml \ No newline at end of file diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index cf14a8f..d20a965 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.14.2 as ckanbuild +FROM alpine:3.13.7 as ckanbuild # Used by Github Actions to tag the image with ENV IMAGE_TAG=2.9.5 @@ -17,11 +17,9 @@ ENV PIP_SRC=${SRC_DIR} WORKDIR ${SRC_DIR} # Packages to build CKAN requirements and plugins -# Make sure we install python 3.8, cause CKAN is not compatible with 3.9 -RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.13/main \ - python3=3.8.10-r0 \ - python3-dev=3.8.10-r0 && \ - apk add --no-cache \ +RUN apk add --no-cache \ + python3 \ + python3-dev \ git \ curl \ postgresql-dev \ @@ -68,7 +66,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.14.2 as extbuild +FROM alpine:3.13.7 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -81,11 +79,9 @@ ENV DEFAULT_EXTENSIONS envvars ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars ENV ENVVARS_GIT_BRANCH=0.0.1 -# Make sure we install python 3.8, cause CKAN is not compatible with 3.9 -RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.13/main \ - python3=3.8.10-r0 \ - python3-dev=3.8.10-r0 && \ - apk add --no-cache \ +RUN apk add --no-cache \ + python3 \ + python3-dev \ git \ curl @@ -105,7 +101,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.14.2 +FROM alpine:3.13.7 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan @@ -119,10 +115,8 @@ ENV CKAN_SITE_URL=http://localhost:5000 ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher # Install necessary packages to run CKAN -# Make sure we install python 3.8, cause CKAN is not compatible with 3.9 -RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.13/main \ - python3=3.8.10-r0 && \ - apk add --no-cache git \ +RUN apk add --no-cache \ + python3 \ bash \ gettext \ curl \ diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 87cd417..c8edbcb 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -118,6 +118,9 @@ RUN mkdir -p ${SRC_DIR} RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ python ${SRC_DIR}/get-pip.py +# Downgrade setuptools so that CKAN requirements can be built +RUN pip install setuptools==44.1.0 + # Fetch and build the default CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars @@ -180,6 +183,9 @@ RUN apt-get update && \ RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ python ${SRC_DIR}/get-pip.py +# Downgrade setuptools so that CKAN requirements can be built +RUN pip install setuptools==44.1.0 + # Get artifacts from build stages COPY --from=ckanbuild /wheels /srv/app/wheels COPY --from=extbuild /wheels /srv/app/ext_wheels From 07919aad77ce9ea1d2b34283842beb274ca0f0f5 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Thu, 20 Jan 2022 15:44:54 +0100 Subject: [PATCH 107/213] * base image is now alpine:3.13 which uses python 3.8 by default --- images/datapusher/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/images/datapusher/Dockerfile b/images/datapusher/Dockerfile index 352bf95..f425cfb 100644 --- a/images/datapusher/Dockerfile +++ b/images/datapusher/Dockerfile @@ -1,7 +1,7 @@ ############# ### Build ### ############# -FROM alpine:3.14.2 as build +FROM alpine:3.13.7 as build # Used by Github Actions to tag the image with ENV IMAGE_TAG=0.0.17 @@ -56,7 +56,7 @@ RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.1 ############ ### MAIN ### ############ -FROM alpine:3.14.2 +FROM alpine:3.13.7 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan From 1bde13293caf500e1b25d04dff25aebea563315a Mon Sep 17 00:00:00 2001 From: Blagoja Stojkoski Date: Fri, 21 Jan 2022 11:05:46 +0100 Subject: [PATCH 108/213] Revert "check_solr_connection bug fix" --- images/ckan/2.9/setup/app/prerun.py | 1 - 1 file changed, 1 deletion(-) diff --git a/images/ckan/2.9/setup/app/prerun.py b/images/ckan/2.9/setup/app/prerun.py index 2dc1301..f31c719 100644 --- a/images/ckan/2.9/setup/app/prerun.py +++ b/images/ckan/2.9/setup/app/prerun.py @@ -83,7 +83,6 @@ def check_solr_connection(retry=None): conn_info = connection.read() # SolrCloud conn_info = re.sub(r'"zkConnected":true', '"zkConnected":True', conn_info.decode('utf-8')) - conn_info = re.sub(r'"zkConnected":false', '"zkConnected":False', conn_info.decode('utf-8')) eval(conn_info) def init_db(): From 743b5c393e6331eefc29541120d14a2b564e7142 Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Tue, 25 Jan 2022 16:14:07 +0100 Subject: [PATCH 109/213] Add SOLR8 in compose setup since CKAN 2.9.5 supports it --- Readme.md | 6 +- compose/{ => 2.8}/.ckan-env | 0 compose/{ => 2.8}/.env | 2 +- compose/{ => 2.8}/docker-compose.yml | 0 compose/{ => 2.8}/postgresql/Dockerfile | 0 .../00_create_datastore.sh | 0 .../20_postgis_permissions.sql | 0 compose/{ => 2.8}/solr/Dockerfile | 0 compose/{ => 2.8}/solr/solrconfig-2.7.12.xml | 0 compose/{ => 2.8}/solr/solrconfig-2.8.10.xml | 0 compose/{ => 2.8}/solr/solrconfig-2.9.5.xml | 0 compose/2.9/.ckan-env | 36 +++++++ compose/2.9/.env | 35 +++++++ compose/2.9/docker-compose.yml | 94 +++++++++++++++++++ compose/2.9/postgresql/Dockerfile | 13 +++ .../00_create_datastore.sh | 8 ++ .../20_postgis_permissions.sql | 3 + compose/2.9/solr8/ckan_init_solr.sh | 38 ++++++++ examples/harvest/.ckan-env | 4 + examples/harvest/.env | 3 + examples/harvest/Dockerfile | 8 +- examples/harvest/docker-compose.yml | 20 ++-- examples/s3filestore/.ckan-env | 4 + examples/s3filestore/.env | 3 + examples/s3filestore/Dockerfile | 2 +- examples/s3filestore/docker-compose.yml | 16 ++-- images/ckan/2.7/Dockerfile | 1 + images/ckan/2.8/Dockerfile | 1 + images/ckan/2.9/Dockerfile | 1 + images/ckan/2.9/setup/app/prerun.py | 14 ++- 30 files changed, 281 insertions(+), 31 deletions(-) rename compose/{ => 2.8}/.ckan-env (100%) rename compose/{ => 2.8}/.env (97%) rename compose/{ => 2.8}/docker-compose.yml (100%) rename compose/{ => 2.8}/postgresql/Dockerfile (100%) rename compose/{ => 2.8}/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh (100%) rename compose/{ => 2.8}/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql (100%) rename compose/{ => 2.8}/solr/Dockerfile (100%) rename compose/{ => 2.8}/solr/solrconfig-2.7.12.xml (100%) rename compose/{ => 2.8}/solr/solrconfig-2.8.10.xml (100%) rename compose/{ => 2.8}/solr/solrconfig-2.9.5.xml (100%) create mode 100644 compose/2.9/.ckan-env create mode 100644 compose/2.9/.env create mode 100644 compose/2.9/docker-compose.yml create mode 100644 compose/2.9/postgresql/Dockerfile create mode 100644 compose/2.9/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh create mode 100644 compose/2.9/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql create mode 100755 compose/2.9/solr8/ckan_init_solr.sh diff --git a/Readme.md b/Readme.md index 7c5887d..93b442b 100644 --- a/Readme.md +++ b/Readme.md @@ -29,16 +29,16 @@ Directory layout: ## Running CKAN using docker-compose To start CKAN using docker-compose, simply change into the *compose* directory and run ```sh -cd compose +cd compose/2.9 docker-compose build docker-compose up ``` Check if CKAN was succesfuly started on http://localhost:5000. ### Configuration -In order to configure CKAN within docker-compose we use both build/up time variables loaded via the [.env](./compose/.env) file, and runtime variables loaded via the [.ckan-env](./compose/.ckan-env) file. +In order to configure CKAN within docker-compose we use both build/up time variables loaded via the [.env](./compose/2.9/.env) file, and runtime variables loaded via the [.ckan-env](./compose/2.9/.ckan-env) file. -Variables in the [.env](./compose/.env) file are loaded when running `docker-compose build` and `docker-compose up`, while variables in [.ckan-env](./compose/.ckan-env) file are used withing the CKAN container at runtime to configure CKAN and CKAN extensions using [ckanext-envvars](https://github.com/okfn/ckanext-envvars). +Variables in the [.env](./compose/2.9/.env) file are loaded when running `docker-compose build` and `docker-compose up`, while variables in [.ckan-env](./compose/2.9/.ckan-env) file are used withing the CKAN container at runtime to configure CKAN and CKAN extensions using [ckanext-envvars](https://github.com/okfn/ckanext-envvars). ## Extending CKAN docker images Check some examples of extending CKAN docker images in the [examples](./examples) directory. diff --git a/compose/.ckan-env b/compose/2.8/.ckan-env similarity index 100% rename from compose/.ckan-env rename to compose/2.8/.ckan-env diff --git a/compose/.env b/compose/2.8/.env similarity index 97% rename from compose/.env rename to compose/2.8/.env index 8eb7389..9d90e47 100644 --- a/compose/.env +++ b/compose/2.8/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.5 +CKAN_VERSION=2.8.10 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/docker-compose.yml b/compose/2.8/docker-compose.yml similarity index 100% rename from compose/docker-compose.yml rename to compose/2.8/docker-compose.yml diff --git a/compose/postgresql/Dockerfile b/compose/2.8/postgresql/Dockerfile similarity index 100% rename from compose/postgresql/Dockerfile rename to compose/2.8/postgresql/Dockerfile diff --git a/compose/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh b/compose/2.8/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh similarity index 100% rename from compose/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh rename to compose/2.8/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh diff --git a/compose/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql b/compose/2.8/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql similarity index 100% rename from compose/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql rename to compose/2.8/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql diff --git a/compose/solr/Dockerfile b/compose/2.8/solr/Dockerfile similarity index 100% rename from compose/solr/Dockerfile rename to compose/2.8/solr/Dockerfile diff --git a/compose/solr/solrconfig-2.7.12.xml b/compose/2.8/solr/solrconfig-2.7.12.xml similarity index 100% rename from compose/solr/solrconfig-2.7.12.xml rename to compose/2.8/solr/solrconfig-2.7.12.xml diff --git a/compose/solr/solrconfig-2.8.10.xml b/compose/2.8/solr/solrconfig-2.8.10.xml similarity index 100% rename from compose/solr/solrconfig-2.8.10.xml rename to compose/2.8/solr/solrconfig-2.8.10.xml diff --git a/compose/solr/solrconfig-2.9.5.xml b/compose/2.8/solr/solrconfig-2.9.5.xml similarity index 100% rename from compose/solr/solrconfig-2.9.5.xml rename to compose/2.8/solr/solrconfig-2.9.5.xml diff --git a/compose/2.9/.ckan-env b/compose/2.9/.ckan-env new file mode 100644 index 0000000..ceb8509 --- /dev/null +++ b/compose/2.9/.ckan-env @@ -0,0 +1,36 @@ +# Runtime configuration of CKAN enabled through ckanext-envvars +# Information about how it works: https://github.com/okfn/ckanext-envvars +# Note that variables here take presedence over build/up time variables in .env + +# Set to true to disable CKAN from starting and serve a maintenance page +MAINTENANCE_MODE=false + +# General Settings +CKAN_SITE_ID=default +CKAN_SITE_URL=http://localhost:5000 +CKAN_PORT=5000 +CKAN_MAX_UPLOAD_SIZE_MB=10 +# CKAN Plugins +CKAN__PLUGINS=envvars image_view text_view recline_view datastore datapusher +# CKAN requires storage path to be set in order for filestore to be enabled +CKAN__STORAGE_PATH=/srv/app/data +CKAN__WEBASSETS__PATH=/srv/app/data/webassets +# SYSADMIN settings, a sysadmin user is created automatically with the below credentials +CKAN_SYSADMIN_NAME=sysadmin +CKAN_SYSADMIN_PASSWORD=password +CKAN_SYSADMIN_EMAIL=sysadmin@ckantest.com + +# Email settings +CKAN_SMTP_SERVER=smtp.corporateict.domain:25 +CKAN_SMTP_STARTTLS=True +CKAN_SMTP_USER=user +CKAN_SMTP_PASSWORD=pass +CKAN_SMTP_MAIL_FROM=ckan@localhost + +# Datapusher configuration +CKAN__DATAPUSHER__URL=http://datapusher:8000 +CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000/ + +# Solr configuration +CKAN_VERSION=2.9.5 +CKAN_CORE_NAME=ckan diff --git a/compose/2.9/.env b/compose/2.9/.env new file mode 100644 index 0000000..822c1bf --- /dev/null +++ b/compose/2.9/.env @@ -0,0 +1,35 @@ +# Variables in this file will be used as build arguments when running +# docker-compose build and docker-compose up +# Verify correct substitution with "docker-compose config" +# If variables are newly added or enabled, please delete and rebuild the images to pull in changes: +# docker-compose down -v +# docker-compose build +# docker-compose up -d + +# Database +POSTGRES_PASSWORD=ckan +POSTGRES_PORT=5432 +DATASTORE_READONLY_PASSWORD=datastore + +# CKAN +CKAN_VERSION=2.9.5 +CKAN_SITE_ID=default +CKAN_SITE_URL=http://localhost:5000 +CKAN_PORT=5000 +CKAN_MAX_UPLOAD_SIZE_MB=10 + +# Datapusher +DATAPUSHER_VERSION=0.0.17 +DATAPUSHER_MAX_CONTENT_LENGTH=10485760 +DATAPUSHER_CHUNK_SIZE=16384 +DATAPUSHER_CHUNK_INSERT_ROWS=250 +DATAPUSHER_DOWNLOAD_TIMEOUT=30 +DATAPUSHER_SSL_VERIFY=False +DATAPUSHER_REWRITE_RESOURCES=True +DATAPUSHER_REWRITE_URL=http://ckan:5000 + +# SOLR +CKAN_CORE_NAME=ckan + +# Redis +REDIS_VERSION=6.0.7 diff --git a/compose/2.9/docker-compose.yml b/compose/2.9/docker-compose.yml new file mode 100644 index 0000000..8eac2a7 --- /dev/null +++ b/compose/2.9/docker-compose.yml @@ -0,0 +1,94 @@ +# docker-compose build && docker-compose up -d +version: "3" + +volumes: + ckan_data: + pg_data: + solr_data: + +services: + ckan: + container_name: ckan + image: ghcr.io/keitaroinc/ckan:${CKAN_VERSION} + networks: + - frontend + - backend + depends_on: + - db + ports: + - "0.0.0.0:${CKAN_PORT}:5000" + env_file: + - ./.ckan-env + environment: + - CKAN_SQLALCHEMY_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/ckan + - CKAN_DATASTORE_WRITE_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/datastore + - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore + - CKAN_SOLR_URL=http://solr:8983/solr/ckan + - CKAN_REDIS_URL=redis://redis:6379/1 + - CKAN_SITE_URL=${CKAN_SITE_URL} + - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + volumes: + - ckan_data:/srv/app/data + + datapusher: + container_name: datapusher + image: ghcr.io/keitaroinc/datapusher:${DATAPUSHER_VERSION} + networks: + - frontend + - backend + ports: + - "8000:8000" + environment: + - DATAPUSHER_MAX_CONTENT_LENGTH=${DATAPUSHER_MAX_CONTENT_LENGTH} + - DATAPUSHER_CHUNK_SIZE=${DATAPUSHER_CHUNK_SIZE} + - DATAPUSHER_CHUNK_INSERT_ROWS=${DATAPUSHER_CHUNK_INSERT_ROWS} + - DATAPUSHER_DOWNLOAD_TIMEOUT=${DATAPUSHER_DOWNLOAD_TIMEOUT} + - DATAPUSHER_SSL_VERIFY=${DATA_PUSHER_SSL_VERIFY} + - DATAPUSHER_REWRITE_RESOURCES=${DATAPUSHER_REWRITE_RESOURCES} + - DATAPUSHER_REWRITE_URL=${DATAPUSHER_REWRITE_URL} + + + db: + container_name: db + build: + context: . + dockerfile: postgresql/Dockerfile + args: + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + networks: + - backend + environment: + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - PGDATA=/var/lib/postgresql/data/db + volumes: + - pg_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "ckan"] + + solr: + container_name: solr + image: solr:8.11.1 + networks: + - backend + env_file: + - ./.ckan-env + environment: + - CKAN_CORE_NAME=${CKAN_CORE_NAME} + - CKAN_VERSION=${CKAN_VERSION} + volumes: + - solr_data:/var/solr + - ${PWD}/solr8/ckan_init_solr.sh:/docker-entrypoint-initdb.d/ckan_init_solr.sh + + redis: + container_name: redis + image: redis:${REDIS_VERSION} + networks: + - backend + +networks: + frontend: + backend: diff --git a/compose/2.9/postgresql/Dockerfile b/compose/2.9/postgresql/Dockerfile new file mode 100644 index 0000000..d2dee7f --- /dev/null +++ b/compose/2.9/postgresql/Dockerfile @@ -0,0 +1,13 @@ +FROM mdillon/postgis:11 + +# Allow connections; we don't map out any ports so only linked docker containers can connect +RUN echo "host all all 0.0.0.0/0 md5" >> /var/lib/postgresql/data/pg_hba.conf + +# Customize default user/pass/db +ENV POSTGRES_DB ckan +ENV POSTGRES_USER ckan +ARG POSTGRES_PASSWORD +ARG DS_RO_PASS + +# Include datastore setup scripts +COPY ./postgresql/docker-entrypoint-initdb.d /docker-entrypoint-initdb.d diff --git a/compose/2.9/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh b/compose/2.9/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh new file mode 100644 index 0000000..ec1b3c3 --- /dev/null +++ b/compose/2.9/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + CREATE ROLE datastore_ro NOSUPERUSER NOCREATEDB NOCREATEROLE LOGIN PASSWORD '$DS_RO_PASS'; + CREATE DATABASE datastore OWNER ckan ENCODING 'utf-8'; + GRANT ALL PRIVILEGES ON DATABASE datastore TO ckan; +EOSQL \ No newline at end of file diff --git a/compose/2.9/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql b/compose/2.9/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql new file mode 100644 index 0000000..0eb2f85 --- /dev/null +++ b/compose/2.9/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql @@ -0,0 +1,3 @@ +CREATE EXTENSION POSTGIS; +ALTER VIEW geometry_columns OWNER TO ckan; +ALTER TABLE spatial_ref_sys OWNER TO ckan; diff --git a/compose/2.9/solr8/ckan_init_solr.sh b/compose/2.9/solr8/ckan_init_solr.sh new file mode 100755 index 0000000..fb8b8c1 --- /dev/null +++ b/compose/2.9/solr8/ckan_init_solr.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# +# Initialize SOLR for CKAN by creating a ckan core +# Arguments are supplied via environment variables: CKAN_CORE_NAME CKAN_VERSION +# Example: +# CKAN_CORE_NAME=ckan +# CKAN_VERSION=2.9.5 + +set -e + +CKAN_SOLR_SCHEMA_URL=https://raw.githubusercontent.com/ckan/ckan/ckan-$CKAN_VERSION/ckan/config/solr/schema.solr8.xml + +echo "Check whether managed schema exists for CKAN $CKAN_VERSION" +if ! curl --output /dev/null --silent --head --fail "$CKAN_SOLR_SCHEMA_URL"; then + echo "Can't find CKAN SOLR schema at URL: $CKAN_SOLR_SCHEMA_URL. Exiting..." + exit 1 +fi + +echo "Check whether SOLR is initialized for CKAN" +CORESDIR=/var/solr/data + +COREDIR="$CORESDIR/$CKAN_CORE_NAME" +if [ -d "$COREDIR" ]; then + echo "SOLR already initialized, skipping initialization" +else + echo "Initializing SOLR core $CKAN_CORE_NAME for CKAN $CKAN_VERSION" + # init script for handling an empty /var/solr + /opt/docker-solr/scripts/init-var-solr + + # Precreate CKAN core + /opt/docker-solr/scripts/precreate-core $CKAN_CORE_NAME + + # Replace the managed schema with CKANs schema + echo "Adding CKAN managed schema" + curl $CKAN_SOLR_SCHEMA_URL -o /var/solr/data/$CKAN_CORE_NAME/conf/managed-schema -s + + echo "SOLR initialized" +fi diff --git a/examples/harvest/.ckan-env b/examples/harvest/.ckan-env index 3801b91..ba51f22 100644 --- a/examples/harvest/.ckan-env +++ b/examples/harvest/.ckan-env @@ -31,3 +31,7 @@ CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000/ # Harvest settings CKAN__HARVEST__MQ__TYPE=redis CKAN__HARVEST__MQ__HOSTNAME=redis + +# Solr configuration +CKAN_VERSION=2.9.5 +CKAN_CORE_NAME=ckan diff --git a/examples/harvest/.env b/examples/harvest/.env index 8eb7389..822c1bf 100644 --- a/examples/harvest/.env +++ b/examples/harvest/.env @@ -28,5 +28,8 @@ DATAPUSHER_SSL_VERIFY=False DATAPUSHER_REWRITE_RESOURCES=True DATAPUSHER_REWRITE_URL=http://ckan:5000 +# SOLR +CKAN_CORE_NAME=ckan + # Redis REDIS_VERSION=6.0.7 diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index 0975804..5914b3d 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -5,16 +5,14 @@ FROM ghcr.io/keitaroinc/ckan:2.9.5 as extbuild # Locations and tags, please use specific tags or revisions ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest -ENV HARVEST_GIT_BRANCH=v1.3.1 +ENV HARVEST_GIT_BRANCH=v1.3.4 # Switch to the root user USER root # Install necessary packages to build extensions -# Make sure we install python 3.8, cause CKAN is not compatible with 3.9 -RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/v3.13/main \ - python3-dev=3.8.10-r0 && \ - apk add --no-cache \ +RUN apk add --no-cache \ + python3-dev \ gcc \ g++ \ libffi-dev \ diff --git a/examples/harvest/docker-compose.yml b/examples/harvest/docker-compose.yml index f0f2d5f..3135b05 100644 --- a/examples/harvest/docker-compose.yml +++ b/examples/harvest/docker-compose.yml @@ -37,7 +37,7 @@ services: container_name: ckan-harvest-gather build: context: . - command: ckan -c /srv/app/production.ini harvester gather_consumer + command: ckan -c /srv/app/production.ini harvester gather-consumer restart: on-failure networks: - frontend @@ -62,7 +62,7 @@ services: container_name: ckan-harvest-fetch build: context: . - command: ckan -c /srv/app/production.ini harvester fetch_consumer + command: ckan -c /srv/app/production.ini harvester fetch-consumer restart: on-failure networks: - frontend @@ -151,7 +151,7 @@ services: db: container_name: db build: - context: ../../compose + context: ../../compose/2.9 dockerfile: postgresql/Dockerfile args: - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} @@ -169,15 +169,17 @@ services: solr: container_name: solr - build: - context: ../../compose - dockerfile: solr/Dockerfile - args: - - CKAN_VERSION=${CKAN_VERSION} + image: solr:8.11.1 networks: - backend + env_file: + - ./.ckan-env + environment: + - CKAN_CORE_NAME=${CKAN_CORE_NAME} + - CKAN_VERSION=${CKAN_VERSION} volumes: - - solr_data:/opt/solr/server/solr/ckan/data + - solr_data:/var/solr + - ${PWD}/../../compose/2.9/solr8/ckan_init_solr.sh:/docker-entrypoint-initdb.d/ckan_init_solr.sh redis: container_name: redis diff --git a/examples/s3filestore/.ckan-env b/examples/s3filestore/.ckan-env index b862517..7ef13f8 100644 --- a/examples/s3filestore/.ckan-env +++ b/examples/s3filestore/.ckan-env @@ -36,3 +36,7 @@ CKANEXT__S3FILESTORE__AWS_BUCKET_NAME=ckan CKANEXT__S3FILESTORE__HOST_NAME=http://minio:9000 CKANEXT__S3FILESTORE__REGION_NAME=us-east-1 CKANEXT__S3FILESTORE__SIGNATURE_VERSION=s3v4 + +# Solr configuration +CKAN_VERSION=2.9.5 +CKAN_CORE_NAME=ckan diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env index 8eb7389..822c1bf 100644 --- a/examples/s3filestore/.env +++ b/examples/s3filestore/.env @@ -28,5 +28,8 @@ DATAPUSHER_SSL_VERIFY=False DATAPUSHER_REWRITE_RESOURCES=True DATAPUSHER_REWRITE_URL=http://ckan:5000 +# SOLR +CKAN_CORE_NAME=ckan + # Redis REDIS_VERSION=6.0.7 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index c8e6a7e..e4f8748 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -5,7 +5,7 @@ FROM ghcr.io/keitaroinc/ckan:2.9.5 as extbuild # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore -ENV S3FILESTORE_GIT_BRANCH=master +ENV S3FILESTORE_GIT_BRANCH=v1.0.0 # Switch to the root user USER root diff --git a/examples/s3filestore/docker-compose.yml b/examples/s3filestore/docker-compose.yml index d3bdb4c..9f78394 100644 --- a/examples/s3filestore/docker-compose.yml +++ b/examples/s3filestore/docker-compose.yml @@ -51,7 +51,7 @@ services: db: container_name: db build: - context: ../../compose + context: ../../compose/2.9 dockerfile: postgresql/Dockerfile args: - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} @@ -69,15 +69,17 @@ services: solr: container_name: solr - build: - context: ../../compose - dockerfile: solr/Dockerfile - args: - - CKAN_VERSION=${CKAN_VERSION} + image: solr:8.11.1 networks: - backend + env_file: + - ./.ckan-env + environment: + - CKAN_CORE_NAME=${CKAN_CORE_NAME} + - CKAN_VERSION=${CKAN_VERSION} volumes: - - solr_data:/opt/solr/server/solr/ckan/data + - solr_data:/var/solr + - ${PWD}/../../compose/2.9/solr8/ckan_init_solr.sh:/docker-entrypoint-initdb.d/ckan_init_solr.sh redis: container_name: redis diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index c9b1817..3474e4e 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -113,6 +113,7 @@ ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher RUN apk add --no-cache \ git \ bash \ + git \ gettext \ curl \ python2 \ diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index fc47399..c1d940c 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -108,6 +108,7 @@ ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher # Install necessary packages to run CKAN RUN apk add --no-cache git \ bash \ + git \ gettext \ curl \ postgresql-client \ diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index d20a965..d68240e 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -118,6 +118,7 @@ ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher RUN apk add --no-cache \ python3 \ bash \ + git \ gettext \ curl \ postgresql-client \ diff --git a/images/ckan/2.9/setup/app/prerun.py b/images/ckan/2.9/setup/app/prerun.py index f31c719..f07ba1f 100644 --- a/images/ckan/2.9/setup/app/prerun.py +++ b/images/ckan/2.9/setup/app/prerun.py @@ -21,6 +21,7 @@ import psycopg2 from sqlalchemy.engine.url import make_url import urllib.request, urllib.error, urllib.parse import re +import json import time @@ -69,7 +70,7 @@ def check_solr_connection(retry=None): sys.exit(1) url = os.environ.get('CKAN_SOLR_URL', '') - search_url = '{url}/select/?q=*&wt=json'.format(url=url) + search_url = '{url}/schema/name?wt=json'.format(url=url) try: connection = urllib.request.urlopen(search_url) @@ -81,10 +82,13 @@ def check_solr_connection(retry=None): else: import re conn_info = connection.read() - # SolrCloud - conn_info = re.sub(r'"zkConnected":true', '"zkConnected":True', conn_info.decode('utf-8')) - eval(conn_info) - + schema_name = json.loads(conn_info) + if 'ckan' in schema_name['name']: + print('[prerun] Succesfully connected to solr and CKAN schema loaded') + else: + print('[prerun] Succesfully connected to solr, but CKAN schema not found') + sys.exit(1) + def init_db(): print('[prerun] Start init_db...') From 881247cf7007e475ccf70d343df4da5efe6403bb Mon Sep 17 00:00:00 2001 From: Marko Bocevski Date: Tue, 1 Feb 2022 12:19:48 +0100 Subject: [PATCH 110/213] Switch postgres image to use the official postgis image --- compose/2.8/postgresql/Dockerfile | 2 +- .../docker-entrypoint-initdb.d/20_postgis_permissions.sql | 1 - compose/2.9/postgresql/Dockerfile | 2 +- .../docker-entrypoint-initdb.d/20_postgis_permissions.sql | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/compose/2.8/postgresql/Dockerfile b/compose/2.8/postgresql/Dockerfile index d2dee7f..e5fcbb7 100644 --- a/compose/2.8/postgresql/Dockerfile +++ b/compose/2.8/postgresql/Dockerfile @@ -1,4 +1,4 @@ -FROM mdillon/postgis:11 +FROM postgis/postgis:14-3.2-alpine # Allow connections; we don't map out any ports so only linked docker containers can connect RUN echo "host all all 0.0.0.0/0 md5" >> /var/lib/postgresql/data/pg_hba.conf diff --git a/compose/2.8/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql b/compose/2.8/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql index 0eb2f85..8b5348e 100644 --- a/compose/2.8/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql +++ b/compose/2.8/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql @@ -1,3 +1,2 @@ -CREATE EXTENSION POSTGIS; ALTER VIEW geometry_columns OWNER TO ckan; ALTER TABLE spatial_ref_sys OWNER TO ckan; diff --git a/compose/2.9/postgresql/Dockerfile b/compose/2.9/postgresql/Dockerfile index d2dee7f..e5fcbb7 100644 --- a/compose/2.9/postgresql/Dockerfile +++ b/compose/2.9/postgresql/Dockerfile @@ -1,4 +1,4 @@ -FROM mdillon/postgis:11 +FROM postgis/postgis:14-3.2-alpine # Allow connections; we don't map out any ports so only linked docker containers can connect RUN echo "host all all 0.0.0.0/0 md5" >> /var/lib/postgresql/data/pg_hba.conf diff --git a/compose/2.9/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql b/compose/2.9/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql index 0eb2f85..8b5348e 100644 --- a/compose/2.9/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql +++ b/compose/2.9/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql @@ -1,3 +1,2 @@ -CREATE EXTENSION POSTGIS; ALTER VIEW geometry_columns OWNER TO ckan; ALTER TABLE spatial_ref_sys OWNER TO ckan; From 8dfd01c5f4f7f92b28c6a409f2fc17b72bdc4950 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Mon, 14 Mar 2022 17:55:15 +0100 Subject: [PATCH 111/213] Integration for solr auth connection --- images/ckan/2.9/setup/app/prerun.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/images/ckan/2.9/setup/app/prerun.py b/images/ckan/2.9/setup/app/prerun.py index f07ba1f..84306de 100644 --- a/images/ckan/2.9/setup/app/prerun.py +++ b/images/ckan/2.9/setup/app/prerun.py @@ -1,12 +1,9 @@ """ Copyright (c) 2016 Keitaro AB - Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - https://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,7 +16,7 @@ import sys import subprocess import psycopg2 from sqlalchemy.engine.url import make_url -import urllib.request, urllib.error, urllib.parse +import urllib.request, urllib.error, urllib.parse, base64 import re import json @@ -69,11 +66,18 @@ def check_solr_connection(retry=None): print('[prerun] Giving up after 5 tries...') sys.exit(1) - url = os.environ.get('CKAN_SOLR_URL', '') + url = os.environ.get('CKAN_SOLR_URL_AUTH', '') + username = os.environ.get('SOLR_ADMIN_USERNAME', '') + password = os.environ.get('SOLR_ADMIN_PASSWORD', '') search_url = '{url}/schema/name?wt=json'.format(url=url) + + try: - connection = urllib.request.urlopen(search_url) + request = urllib.request.Request(search_url) + base64string = base64.b64encode(bytes('%s:%s' % (username, password),'ascii')) + request.add_header("Authorization", "Basic %s" % base64string.decode('utf-8')) + connection = urllib.request.urlopen(request) except urllib.error.URLError as e: print('[prerun] Unable to connect to solr...try again in a while.') import time @@ -217,4 +221,4 @@ if __name__ == '__main__': init_db() if os.environ.get('CKAN_DATASTORE_WRITE_URL'): init_datastore() - create_sysadmin() + create_sysadmin() \ No newline at end of file From e1da27038ceddf84176dce6963830cfb8164c380 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Tue, 15 Mar 2022 15:25:52 +0100 Subject: [PATCH 112/213] added if statment --- images/ckan/2.9/setup/app/prerun.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/images/ckan/2.9/setup/app/prerun.py b/images/ckan/2.9/setup/app/prerun.py index 84306de..626c42b 100644 --- a/images/ckan/2.9/setup/app/prerun.py +++ b/images/ckan/2.9/setup/app/prerun.py @@ -11,6 +11,7 @@ See the License for the specific language governing permissions and limitations under the License. """ +from multiprocessing import connection import os import sys import subprocess @@ -66,18 +67,21 @@ def check_solr_connection(retry=None): print('[prerun] Giving up after 5 tries...') sys.exit(1) - url = os.environ.get('CKAN_SOLR_URL_AUTH', '') + url = os.environ.get('CKAN_SOLR_URL', '') username = os.environ.get('SOLR_ADMIN_USERNAME', '') password = os.environ.get('SOLR_ADMIN_PASSWORD', '') search_url = '{url}/schema/name?wt=json'.format(url=url) - + try: - request = urllib.request.Request(search_url) - base64string = base64.b64encode(bytes('%s:%s' % (username, password),'ascii')) - request.add_header("Authorization", "Basic %s" % base64string.decode('utf-8')) - connection = urllib.request.urlopen(request) + if not username: + connection = urllib.request.urlopen(search_url) + else: + request = urllib.request.Request(search_url) + base64string = base64.b64encode(bytes('%s:%s' % (username, password),'ascii')) + request.add_header("Authorization", "Basic %s" % base64string.decode('utf-8')) + connection = urllib.request.urlopen(request) except urllib.error.URLError as e: print('[prerun] Unable to connect to solr...try again in a while.') import time From df1c009836b021fe6aee80576cbd740e283f91f5 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Wed, 6 Jul 2022 12:12:19 +0200 Subject: [PATCH 113/213] var for uswgi num of spawns --- images/ckan/2.9/setup/app/start_ckan.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index fee6dad..f5d1b87 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -13,7 +13,12 @@ then fi # Set the common uwsgi options -UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid ckan --gid ckan --http :5000 --master --enable-threads --wsgi-file /srv/app/wsgi.py --module wsgi:application --lazy-apps --gevent 2000 -p 2 -L --gevent-early-monkey-patch --vacuum --harakiri 50 --callable application" +if [ ${UWSGI_PROC_NO} == "0" ] || [ ${UWSGI_PROC_NO} == "null" ] || [ ${UWSGI_PROC_NO} == "" ]; + then + echo setting USWGI proc to 2 + ${UWSGI_PROC_NO}=2 +fi +UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid ckan --gid ckan --http :5000 --master --enable-threads --wsgi-file /srv/app/wsgi.py --module wsgi:application --lazy-apps --gevent 2000 -p ${UWSGI_PROC_NO} -L --gevent-early-monkey-patch --vacuum --harakiri 50 --callable application" # Run the prerun script to init CKAN and create the default admin user python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } From b63f61d41a98366317ad77f7fb2b2603c56c09f7 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Wed, 6 Jul 2022 14:44:05 +0200 Subject: [PATCH 114/213] added var for uswgi workers and if emtpy statment --- images/ckan/2.7/setup/app/start_ckan.sh | 7 ++++++- images/ckan/2.8/setup/app/start_ckan.sh | 7 ++++++- images/ckan/2.9/setup/app/start_ckan.sh | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/images/ckan/2.7/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh index a0bc80d..8dcc548 100755 --- a/images/ckan/2.7/setup/app/start_ckan.sh +++ b/images/ckan/2.7/setup/app/start_ckan.sh @@ -13,7 +13,12 @@ then fi # Set the common uwsgi options -UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --paste-logger /srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L --gevent-early-monkey-patch" +if [ ${UWSGI_PROC_NO} == "0" ] || [ ${UWSGI_PROC_NO} == "" ]; + then + echo setting USWGI proc to 2 + ${UWSGI_PROC_NO}=2 +fi +UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --paste-logger /srv/app/production.ini --lazy-apps --gevent 2000 -p ${UWSGI_PROC_NO} -L --gevent-early-monkey-patch" # Run the prerun script to init CKAN and create the default admin user python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } diff --git a/images/ckan/2.8/setup/app/start_ckan.sh b/images/ckan/2.8/setup/app/start_ckan.sh index a0bc80d..8dcc548 100755 --- a/images/ckan/2.8/setup/app/start_ckan.sh +++ b/images/ckan/2.8/setup/app/start_ckan.sh @@ -13,7 +13,12 @@ then fi # Set the common uwsgi options -UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --paste-logger /srv/app/production.ini --lazy-apps --gevent 2000 -p 2 -L --gevent-early-monkey-patch" +if [ ${UWSGI_PROC_NO} == "0" ] || [ ${UWSGI_PROC_NO} == "" ]; + then + echo setting USWGI proc to 2 + ${UWSGI_PROC_NO}=2 +fi +UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --paste-logger /srv/app/production.ini --lazy-apps --gevent 2000 -p ${UWSGI_PROC_NO} -L --gevent-early-monkey-patch" # Run the prerun script to init CKAN and create the default admin user python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index f5d1b87..c55474a 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -13,7 +13,7 @@ then fi # Set the common uwsgi options -if [ ${UWSGI_PROC_NO} == "0" ] || [ ${UWSGI_PROC_NO} == "null" ] || [ ${UWSGI_PROC_NO} == "" ]; +if [ ${UWSGI_PROC_NO} == "0" ] || [ ${UWSGI_PROC_NO} == "" ]; then echo setting USWGI proc to 2 ${UWSGI_PROC_NO}=2 From d9d581c4f3888b91aa7bd429aea1a07cb80d1c10 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Wed, 6 Jul 2022 16:09:39 +0200 Subject: [PATCH 115/213] * fix gha ubuntu version --- .github/workflows/master_merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index af3ac68..9bbf6ef 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -6,7 +6,7 @@ on: jobs: build-ckan-2-9: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 From 1beede3e180071b5c80bf5f6b36de177281170bd Mon Sep 17 00:00:00 2001 From: Blagoja Stojkoski Date: Wed, 6 Jul 2022 16:12:00 +0200 Subject: [PATCH 116/213] fix gha ubuntu version --- .github/workflows/pr_checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 092597c..caa7ae1 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -6,7 +6,7 @@ on: jobs: build-ckan-2-9: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 From 0c4bcd8919a4429a45c41ff6d42b659ad45b3bdd Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Wed, 6 Jul 2022 16:28:11 +0200 Subject: [PATCH 117/213] reverting gha ubuntu versions --- .github/workflows/master_merge.yml | 2 +- .github/workflows/pr_checks.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index 9bbf6ef..af3ac68 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -6,7 +6,7 @@ on: jobs: build-ckan-2-9: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index caa7ae1..092597c 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -6,7 +6,7 @@ on: jobs: build-ckan-2-9: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 From ff81bdb5d56927205ad053b8681e26fc70a2c3f9 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Wed, 6 Jul 2022 17:30:25 +0200 Subject: [PATCH 118/213] updated uwsgi version --- images/ckan/2.9/Dockerfile | 4 ++-- images/ckan/2.9/Dockerfile.focal | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index d68240e..0a054be 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -60,7 +60,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 +RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.1.2 greenlet==1.1.0 ########################### @@ -143,7 +143,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.1.2 +RUN pip install --no-index --find-links=/srv/app/wheels uWSGI==2.0.20 gevent==21.1.2 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index c8edbcb..569340b 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -81,7 +81,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 +RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.1.2 greenlet==1.1.0 ########################### @@ -192,7 +192,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.1.2 +RUN pip install --no-index --find-links=/srv/app/wheels uWSGI==2.0.20 gevent==21.1.2 # Create a local user and group to run the app RUN groupadd -g 92 ckan && \ From 8af55f61b8b961673b06c90af746da0ac2df52a0 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Thu, 7 Jul 2022 15:22:25 +0200 Subject: [PATCH 119/213] * adds docker buildkit to github action env --- .github/workflows/master_merge.yml | 3 +++ .github/workflows/pr_checks.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index af3ac68..9d581c6 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -4,6 +4,9 @@ on: push: branches: master +env: + DOCKER_BUILDKIT: 1 + jobs: build-ckan-2-9: runs-on: ubuntu-latest diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 092597c..61b165e 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -4,6 +4,9 @@ on: pull_request: branches: master +env: + DOCKER_BUILDKIT: 1 + jobs: build-ckan-2-9: runs-on: ubuntu-latest From 9dee3b987559eb7536455a24776560820600415c Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Thu, 7 Jul 2022 16:09:32 +0200 Subject: [PATCH 120/213] plaintext --- .github/workflows/pr_checks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 61b165e..a90d904 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -55,6 +55,8 @@ jobs: tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-9 cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-9 + build-args: + --progress=plain build-ckan-2-8: runs-on: ubuntu-latest From a67752369aec4be39ed755f20117ead83c2b32a8 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Thu, 7 Jul 2022 16:34:19 +0200 Subject: [PATCH 121/213] test 20.04 --- .github/workflows/pr_checks.yml | 246 ++++++++++++++++---------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index a90d904..c34a7f6 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -9,7 +9,7 @@ env: jobs: build-ckan-2-9: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 @@ -26,20 +26,20 @@ jobs: restore-keys: | ${{ runner.os }}-buildx-2-9 - - name: Get docker tag for Alpine image - id: alpine - run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile)" + # - name: Get docker tag for Alpine image + # id: alpine + # run: | + # echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile)" - - name: Build CKAN 2.9 alpine - uses: docker/build-push-action@v2 - with: - context: ./images/ckan/2.9 - file: ./images/ckan/2.9/Dockerfile - push: false - tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-9 - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-9 + # - name: Build CKAN 2.9 alpine + # uses: docker/build-push-action@v2 + # with: + # context: ./images/ckan/2.9 + # file: ./images/ckan/2.9/Dockerfile + # push: false + # tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + # cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-9 + # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-9 - name: Get docker tag for Ubuntu image id: ubuntu @@ -55,132 +55,132 @@ jobs: tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-9 cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-9 - build-args: + build-args: | --progress=plain - build-ckan-2-8: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 + # build-ckan-2-8: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v1 - - name: Cache Docker layers - uses: actions/cache@v2 - with: - path: | - /tmp/.buildx-cache-alpine-2-8 - /tmp/.buildx-cache-ubuntu-2-8 - key: ${{ runner.os }}-buildx-2-8-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx-2-8 + # - name: Cache Docker layers + # uses: actions/cache@v2 + # with: + # path: | + # /tmp/.buildx-cache-alpine-2-8 + # /tmp/.buildx-cache-ubuntu-2-8 + # key: ${{ runner.os }}-buildx-2-8-${{ github.sha }} + # restore-keys: | + # ${{ runner.os }}-buildx-2-8 - - name: Get docker tag for Alpine image - id: alpine - run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile)" + # - name: Get docker tag for Alpine image + # id: alpine + # run: | + # echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile)" - - name: Build CKAN 2.8 alpine - uses: docker/build-push-action@v2 - with: - context: ./images/ckan/2.8 - file: ./images/ckan/2.8/Dockerfile - push: false - tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-8 - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-8 + # - name: Build CKAN 2.8 alpine + # uses: docker/build-push-action@v2 + # with: + # context: ./images/ckan/2.8 + # file: ./images/ckan/2.8/Dockerfile + # push: false + # tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + # cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-8 + # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-8 - - name: Get docker tag for Ubuntu image - id: ubuntu - run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile.focal)" + # - name: Get docker tag for Ubuntu image + # id: ubuntu + # run: | + # echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile.focal)" - - name: Build CKAN 2.8 ubuntu - uses: docker/build-push-action@v2 - with: - context: ./images/ckan/2.8 - file: ./images/ckan/2.8/Dockerfile.focal - push: false - tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-8 - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-8 + # - name: Build CKAN 2.8 ubuntu + # uses: docker/build-push-action@v2 + # with: + # context: ./images/ckan/2.8 + # file: ./images/ckan/2.8/Dockerfile.focal + # push: false + # tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + # cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-8 + # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-8 - build-ckan-2-7: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 + # build-ckan-2-7: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v1 - - name: Cache Docker layers - uses: actions/cache@v2 - with: - path: | - /tmp/.buildx-cache-alpine-2-7 - /tmp/.buildx-cache-ubuntu-2-7 - key: ${{ runner.os }}-buildx-2-7-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx-2-7 + # - name: Cache Docker layers + # uses: actions/cache@v2 + # with: + # path: | + # /tmp/.buildx-cache-alpine-2-7 + # /tmp/.buildx-cache-ubuntu-2-7 + # key: ${{ runner.os }}-buildx-2-7-${{ github.sha }} + # restore-keys: | + # ${{ runner.os }}-buildx-2-7 - - name: Get docker tag for Alpine image - id: alpine - run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile)" + # - name: Get docker tag for Alpine image + # id: alpine + # run: | + # echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile)" - - name: Build CKAN 2.7 alpine - uses: docker/build-push-action@v2 - with: - context: ./images/ckan/2.7 - file: ./images/ckan/2.7/Dockerfile - push: false - tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-7 - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-7 + # - name: Build CKAN 2.7 alpine + # uses: docker/build-push-action@v2 + # with: + # context: ./images/ckan/2.7 + # file: ./images/ckan/2.7/Dockerfile + # push: false + # tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + # cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-7 + # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-7 - - name: Get docker tag for Ubuntu image - id: ubuntu - run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile.focal)" + # - name: Get docker tag for Ubuntu image + # id: ubuntu + # run: | + # echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile.focal)" - - name: Build CKAN 2.7 ubuntu - uses: docker/build-push-action@v2 - with: - context: ./images/ckan/2.7 - file: ./images/ckan/2.7/Dockerfile.focal - push: false - tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-7 - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-7 + # - name: Build CKAN 2.7 ubuntu + # uses: docker/build-push-action@v2 + # with: + # context: ./images/ckan/2.7 + # file: ./images/ckan/2.7/Dockerfile.focal + # push: false + # tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + # cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-7 + # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-7 - build-ckan-datapusher: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 + # build-ckan-datapusher: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + # - name: Set up Docker Buildx + # uses: docker/setup-buildx-action@v1 - - name: Cache Docker layers - uses: actions/cache@v2 - with: - path: /tmp/.buildx-cache-datapusher - key: ${{ runner.os }}-buildx-datapusher-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx-datapusher + # - name: Cache Docker layers + # uses: actions/cache@v2 + # with: + # path: /tmp/.buildx-cache-datapusher + # key: ${{ runner.os }}-buildx-datapusher-${{ github.sha }} + # restore-keys: | + # ${{ runner.os }}-buildx-datapusher - - name: Get docker tag for datapusher image - id: datapusher - run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/datapusher/Dockerfile)" + # - name: Get docker tag for datapusher image + # id: datapusher + # run: | + # echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/datapusher/Dockerfile)" - - name: Build CKAN datapusher - uses: docker/build-push-action@v2 - with: - context: ./images/datapusher - file: ./images/datapusher/Dockerfile - push: false - tags: keitaro/ckandatapusher:${{ steps.datapusher.outputs.IMAGE_TAG }} - cache-from: type=local,src=/tmp/.buildx-cache-datapusher - cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-datapusher + # - name: Build CKAN datapusher + # uses: docker/build-push-action@v2 + # with: + # context: ./images/datapusher + # file: ./images/datapusher/Dockerfile + # push: false + # tags: keitaro/ckandatapusher:${{ steps.datapusher.outputs.IMAGE_TAG }} + # cache-from: type=local,src=/tmp/.buildx-cache-datapusher + # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-datapusher From c16358a4a9def136dc29d12a64679475671c921a Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Thu, 7 Jul 2022 16:47:43 +0200 Subject: [PATCH 122/213] changed version of ubuntu runner to 20.04 because latest has problems --- .github/workflows/master_merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index 9d581c6..9f9fb2a 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -9,7 +9,7 @@ env: jobs: build-ckan-2-9: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 From c25166f6a3ea2300fa3bc755cf34a3b032bb39fd Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Thu, 7 Jul 2022 16:51:42 +0200 Subject: [PATCH 123/213] changed version of ubuntu runner to 20.04 because latest has problems --- .github/workflows/master_merge.yml | 2 - .github/workflows/pr_checks.yml | 244 ++++++++++++++--------------- 2 files changed, 121 insertions(+), 125 deletions(-) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index 9f9fb2a..37aaf2d 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -4,8 +4,6 @@ on: push: branches: master -env: - DOCKER_BUILDKIT: 1 jobs: build-ckan-2-9: diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index c34a7f6..4619032 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -4,8 +4,6 @@ on: pull_request: branches: master -env: - DOCKER_BUILDKIT: 1 jobs: build-ckan-2-9: @@ -26,20 +24,20 @@ jobs: restore-keys: | ${{ runner.os }}-buildx-2-9 - # - name: Get docker tag for Alpine image - # id: alpine - # run: | - # echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile)" + - name: Get docker tag for Alpine image + id: alpine + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile)" - # - name: Build CKAN 2.9 alpine - # uses: docker/build-push-action@v2 - # with: - # context: ./images/ckan/2.9 - # file: ./images/ckan/2.9/Dockerfile - # push: false - # tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} - # cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-9 - # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-9 + - name: Build CKAN 2.9 alpine + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.9 + file: ./images/ckan/2.9/Dockerfile + push: false + tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-9 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-9 - name: Get docker tag for Ubuntu image id: ubuntu @@ -58,129 +56,129 @@ jobs: build-args: | --progress=plain - # build-ckan-2-8: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v2 + build-ckan-2-8: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 - # - name: Set up Docker Buildx - # uses: docker/setup-buildx-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 - # - name: Cache Docker layers - # uses: actions/cache@v2 - # with: - # path: | - # /tmp/.buildx-cache-alpine-2-8 - # /tmp/.buildx-cache-ubuntu-2-8 - # key: ${{ runner.os }}-buildx-2-8-${{ github.sha }} - # restore-keys: | - # ${{ runner.os }}-buildx-2-8 + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: | + /tmp/.buildx-cache-alpine-2-8 + /tmp/.buildx-cache-ubuntu-2-8 + key: ${{ runner.os }}-buildx-2-8-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-2-8 - # - name: Get docker tag for Alpine image - # id: alpine - # run: | - # echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile)" + - name: Get docker tag for Alpine image + id: alpine + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile)" - # - name: Build CKAN 2.8 alpine - # uses: docker/build-push-action@v2 - # with: - # context: ./images/ckan/2.8 - # file: ./images/ckan/2.8/Dockerfile - # push: false - # tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} - # cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-8 - # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-8 + - name: Build CKAN 2.8 alpine + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.8 + file: ./images/ckan/2.8/Dockerfile + push: false + tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-8 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-8 - # - name: Get docker tag for Ubuntu image - # id: ubuntu - # run: | - # echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile.focal)" + - name: Get docker tag for Ubuntu image + id: ubuntu + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile.focal)" - # - name: Build CKAN 2.8 ubuntu - # uses: docker/build-push-action@v2 - # with: - # context: ./images/ckan/2.8 - # file: ./images/ckan/2.8/Dockerfile.focal - # push: false - # tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} - # cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-8 - # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-8 + - name: Build CKAN 2.8 ubuntu + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.8 + file: ./images/ckan/2.8/Dockerfile.focal + push: false + tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-8 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-8 - # build-ckan-2-7: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v2 + build-ckan-2-7: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 - # - name: Set up Docker Buildx - # uses: docker/setup-buildx-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 - # - name: Cache Docker layers - # uses: actions/cache@v2 - # with: - # path: | - # /tmp/.buildx-cache-alpine-2-7 - # /tmp/.buildx-cache-ubuntu-2-7 - # key: ${{ runner.os }}-buildx-2-7-${{ github.sha }} - # restore-keys: | - # ${{ runner.os }}-buildx-2-7 + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: | + /tmp/.buildx-cache-alpine-2-7 + /tmp/.buildx-cache-ubuntu-2-7 + key: ${{ runner.os }}-buildx-2-7-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-2-7 - # - name: Get docker tag for Alpine image - # id: alpine - # run: | - # echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile)" + - name: Get docker tag for Alpine image + id: alpine + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile)" - # - name: Build CKAN 2.7 alpine - # uses: docker/build-push-action@v2 - # with: - # context: ./images/ckan/2.7 - # file: ./images/ckan/2.7/Dockerfile - # push: false - # tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} - # cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-7 - # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-7 + - name: Build CKAN 2.7 alpine + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.7 + file: ./images/ckan/2.7/Dockerfile + push: false + tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-7 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-7 - # - name: Get docker tag for Ubuntu image - # id: ubuntu - # run: | - # echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile.focal)" + - name: Get docker tag for Ubuntu image + id: ubuntu + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile.focal)" - # - name: Build CKAN 2.7 ubuntu - # uses: docker/build-push-action@v2 - # with: - # context: ./images/ckan/2.7 - # file: ./images/ckan/2.7/Dockerfile.focal - # push: false - # tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} - # cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-7 - # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-7 + - name: Build CKAN 2.7 ubuntu + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.7 + file: ./images/ckan/2.7/Dockerfile.focal + push: false + tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-7 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-7 - # build-ckan-datapusher: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v2 + build-ckan-datapusher: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 - # - name: Set up Docker Buildx - # uses: docker/setup-buildx-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 - # - name: Cache Docker layers - # uses: actions/cache@v2 - # with: - # path: /tmp/.buildx-cache-datapusher - # key: ${{ runner.os }}-buildx-datapusher-${{ github.sha }} - # restore-keys: | - # ${{ runner.os }}-buildx-datapusher + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache-datapusher + key: ${{ runner.os }}-buildx-datapusher-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-datapusher - # - name: Get docker tag for datapusher image - # id: datapusher - # run: | - # echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/datapusher/Dockerfile)" + - name: Get docker tag for datapusher image + id: datapusher + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/datapusher/Dockerfile)" - # - name: Build CKAN datapusher - # uses: docker/build-push-action@v2 - # with: - # context: ./images/datapusher - # file: ./images/datapusher/Dockerfile - # push: false - # tags: keitaro/ckandatapusher:${{ steps.datapusher.outputs.IMAGE_TAG }} - # cache-from: type=local,src=/tmp/.buildx-cache-datapusher - # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-datapusher + - name: Build CKAN datapusher + uses: docker/build-push-action@v2 + with: + context: ./images/datapusher + file: ./images/datapusher/Dockerfile + push: false + tags: keitaro/ckandatapusher:${{ steps.datapusher.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache-datapusher + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-datapusher From f99c987f2b386ba1aefc7172a1747f768825ec42 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Wed, 13 Jul 2022 11:17:25 +0200 Subject: [PATCH 124/213] * fixes uwsgi number of process var check --- images/ckan/2.9/setup/app/start_ckan.sh | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index c55474a..f1b509f 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -12,13 +12,8 @@ then done fi -# Set the common uwsgi options -if [ ${UWSGI_PROC_NO} == "0" ] || [ ${UWSGI_PROC_NO} == "" ]; - then - echo setting USWGI proc to 2 - ${UWSGI_PROC_NO}=2 -fi -UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid ckan --gid ckan --http :5000 --master --enable-threads --wsgi-file /srv/app/wsgi.py --module wsgi:application --lazy-apps --gevent 2000 -p ${UWSGI_PROC_NO} -L --gevent-early-monkey-patch --vacuum --harakiri 50 --callable application" +echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" +UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid ckan --gid ckan --http :5000 --master --enable-threads --wsgi-file /srv/app/wsgi.py --module wsgi:application --lazy-apps --gevent 2000 -p ${UWSGI_PROC_NO:-2} -L --gevent-early-monkey-patch --vacuum --harakiri 50 --callable application" # Run the prerun script to init CKAN and create the default admin user python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } From 7b4bb7b809304cb8542491104a0c800009a6749a Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Tue, 27 Sep 2022 15:20:07 +0200 Subject: [PATCH 125/213] patch releases for CKAN 2.8.11 and 2.9.6 --- Readme.md | 8 ++++---- compose/2.8/.env | 2 +- .../solr/{solrconfig-2.8.10.xml => solrconfig-2.8.11.xml} | 0 .../solr/{solrconfig-2.9.5.xml => solrconfig-2.9.6.xml} | 0 compose/2.9/.ckan-env | 2 +- compose/2.9/.env | 2 +- compose/2.9/solr8/ckan_init_solr.sh | 2 +- examples/harvest/.ckan-env | 2 +- examples/harvest/.env | 2 +- examples/harvest/Dockerfile | 4 ++-- examples/s3filestore/.ckan-env | 2 +- examples/s3filestore/.env | 2 +- examples/s3filestore/Dockerfile | 4 ++-- images/ckan/2.8/Dockerfile | 8 ++++---- images/ckan/2.8/Dockerfile.focal | 8 ++++---- images/ckan/2.9/Dockerfile | 8 ++++---- images/ckan/2.9/Dockerfile.focal | 8 ++++---- 17 files changed, 32 insertions(+), 32 deletions(-) rename compose/2.8/solr/{solrconfig-2.8.10.xml => solrconfig-2.8.11.xml} (100%) rename compose/2.8/solr/{solrconfig-2.9.5.xml => solrconfig-2.9.6.xml} (100%) diff --git a/Readme.md b/Readme.md index 93b442b..1af3cf7 100644 --- a/Readme.md +++ b/Readme.md @@ -48,7 +48,7 @@ We recommend to use a multi-stage approach to extend the docker images that we p ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.5 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.6 as extbuild # Switch to the root user USER root @@ -64,7 +64,7 @@ RUN pip wheel --wheel-dir=/wheels git+https://github.com/acmecorp/ckanext-acme@0 ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.5 +FROM ghcr.io/keitaroinc/ckan:2.9.6 # Add the custom extensions to the plugins list ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher acme @@ -94,9 +94,9 @@ You can add scripts to CKAN custom images and copy them to the *docker-afterinit ## Build To build a CKAN image run: ```sh -docker build --tag ghcr.io/keitaroinc/ckan:2.9.5 images/ckan/2.9 +docker build --tag ghcr.io/keitaroinc/ckan:2.9.6 images/ckan/2.9 ``` -The –-tag ghcr.io/keitaroinc/ckan:2.9.5 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.5 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. +The –-tag ghcr.io/keitaroinc/ckan:2.9.6 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.6 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. ## Upload to DockerHub >*It's recommended to upload built images to DockerHub* diff --git a/compose/2.8/.env b/compose/2.8/.env index 9d90e47..2295d54 100644 --- a/compose/2.8/.env +++ b/compose/2.8/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.8.10 +CKAN_VERSION=2.8.11 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/2.8/solr/solrconfig-2.8.10.xml b/compose/2.8/solr/solrconfig-2.8.11.xml similarity index 100% rename from compose/2.8/solr/solrconfig-2.8.10.xml rename to compose/2.8/solr/solrconfig-2.8.11.xml diff --git a/compose/2.8/solr/solrconfig-2.9.5.xml b/compose/2.8/solr/solrconfig-2.9.6.xml similarity index 100% rename from compose/2.8/solr/solrconfig-2.9.5.xml rename to compose/2.8/solr/solrconfig-2.9.6.xml diff --git a/compose/2.9/.ckan-env b/compose/2.9/.ckan-env index ceb8509..b09867b 100644 --- a/compose/2.9/.ckan-env +++ b/compose/2.9/.ckan-env @@ -32,5 +32,5 @@ CKAN__DATAPUSHER__URL=http://datapusher:8000 CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000/ # Solr configuration -CKAN_VERSION=2.9.5 +CKAN_VERSION=2.9.6 CKAN_CORE_NAME=ckan diff --git a/compose/2.9/.env b/compose/2.9/.env index 822c1bf..462393d 100644 --- a/compose/2.9/.env +++ b/compose/2.9/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.5 +CKAN_VERSION=2.9.6 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/2.9/solr8/ckan_init_solr.sh b/compose/2.9/solr8/ckan_init_solr.sh index fb8b8c1..8483fee 100755 --- a/compose/2.9/solr8/ckan_init_solr.sh +++ b/compose/2.9/solr8/ckan_init_solr.sh @@ -4,7 +4,7 @@ # Arguments are supplied via environment variables: CKAN_CORE_NAME CKAN_VERSION # Example: # CKAN_CORE_NAME=ckan -# CKAN_VERSION=2.9.5 +# CKAN_VERSION=2.9.6 set -e diff --git a/examples/harvest/.ckan-env b/examples/harvest/.ckan-env index ba51f22..33bbc66 100644 --- a/examples/harvest/.ckan-env +++ b/examples/harvest/.ckan-env @@ -33,5 +33,5 @@ CKAN__HARVEST__MQ__TYPE=redis CKAN__HARVEST__MQ__HOSTNAME=redis # Solr configuration -CKAN_VERSION=2.9.5 +CKAN_VERSION=2.9.6 CKAN_CORE_NAME=ckan diff --git a/examples/harvest/.env b/examples/harvest/.env index 822c1bf..462393d 100644 --- a/examples/harvest/.env +++ b/examples/harvest/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.5 +CKAN_VERSION=2.9.6 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index 5914b3d..bcafd12 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.5 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.6 as extbuild # Locations and tags, please use specific tags or revisions ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest @@ -30,7 +30,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.5 +FROM ghcr.io/keitaroinc/ckan:2.9.6 LABEL maintainer="Keitaro Inc " diff --git a/examples/s3filestore/.ckan-env b/examples/s3filestore/.ckan-env index 7ef13f8..918248a 100644 --- a/examples/s3filestore/.ckan-env +++ b/examples/s3filestore/.ckan-env @@ -38,5 +38,5 @@ CKANEXT__S3FILESTORE__REGION_NAME=us-east-1 CKANEXT__S3FILESTORE__SIGNATURE_VERSION=s3v4 # Solr configuration -CKAN_VERSION=2.9.5 +CKAN_VERSION=2.9.6 CKAN_CORE_NAME=ckan diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env index 822c1bf..462393d 100644 --- a/examples/s3filestore/.env +++ b/examples/s3filestore/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.5 +CKAN_VERSION=2.9.6 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index e4f8748..18798ef 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.5 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.6 as extbuild # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore @@ -20,7 +20,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.5 +FROM ghcr.io/keitaroinc/ckan:2.9.6 LABEL maintainer="Keitaro Inc " diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index c1d940c..c6d341a 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.14.2 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.8.10 +ENV IMAGE_TAG=2.8.11 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.10 +ENV GIT_BRANCH=ckan-2.8.11 # Set src dirs ENV SRC_DIR=/srv/app/src @@ -54,7 +54,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.12.0 greenlet==1.1.3 ########################### @@ -132,7 +132,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.1.2 +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.12.0 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index 6ff0a95..02ee63e 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.8.10-focal +ENV IMAGE_TAG=2.8.11-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.10 +ENV GIT_BRANCH=ckan-2.8.11 # Set timezone ENV TZ=UTC @@ -75,7 +75,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.12.0 greenlet==1.1.3 ########################### @@ -174,7 +174,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.1.2 +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.12.0 # Create a local user and group to run the app RUN groupadd -g 92 ckan && \ diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 0a054be..c0ef0c2 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.13.7 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.5 +ENV IMAGE_TAG=2.9.6 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.5 +ENV GIT_BRANCH=ckan-2.9.6 # Set src dirs ENV SRC_DIR=/srv/app/src @@ -60,7 +60,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.1.2 greenlet==1.1.0 +RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.12.0 greenlet==1.1.3 ########################### @@ -143,7 +143,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uWSGI==2.0.20 gevent==21.1.2 +RUN pip install --no-index --find-links=/srv/app/wheels uWSGI==2.0.20 gevent==21.12.0 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 569340b..3b5028c 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.5-focal +ENV IMAGE_TAG=2.9.6-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.5 +ENV GIT_BRANCH=ckan-2.9.6 # Set timezone ENV TZ=UTC @@ -81,7 +81,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.1.2 greenlet==1.1.0 +RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.12.0 greenlet==1.1.3 ########################### @@ -192,7 +192,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uWSGI==2.0.20 gevent==21.1.2 +RUN pip install --no-index --find-links=/srv/app/wheels uWSGI==2.0.20 gevent==21.12.0 # Create a local user and group to run the app RUN groupadd -g 92 ckan && \ From f55459e4e9f75a97ad2dfcf58ec544989ff3d870 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Tue, 27 Sep 2022 15:26:38 +0200 Subject: [PATCH 126/213] updates versions of gevent and greenlet in datapusher image --- images/datapusher/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/images/datapusher/Dockerfile b/images/datapusher/Dockerfile index f425cfb..bd42b0c 100644 --- a/images/datapusher/Dockerfile +++ b/images/datapusher/Dockerfile @@ -50,7 +50,7 @@ RUN pip wheel --wheel-dir=/wheels -r ${REQUIREMENTS_URL} RUN curl -o /wheels/requirements.txt ${REQUIREMENTS_URL} # Get uwsgi and gevent from pip -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==20.6.2 greenlet==0.4.16 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.12.0 greenlet==1.1.3 ############ @@ -80,7 +80,7 @@ RUN curl -o /tmp/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ COPY --from=build /wheels /srv/app/wheels # Install uwsgi and gevent -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==20.6.2 +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.12.0 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ From 24c1c0ee5c23f46628ec5c4fbf991c40e99fde3d Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Tue, 27 Sep 2022 15:52:35 +0200 Subject: [PATCH 127/213] updates versions of gevent and greenlet for CKAN 2.7 --- images/ckan/2.7/Dockerfile | 4 ++-- images/ckan/2.7/Dockerfile.focal | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 3474e4e..9578240 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -58,7 +58,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.12.0 greenlet==1.1.3 ########################### @@ -139,7 +139,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.1.2 +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.12.0 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index bc5c9e2..5e0cfbc 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -83,7 +83,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.1.2 greenlet==1.1.0 +RUN pip wheel --wheel-dir=/wheels uwsgi==2.0.19.1 gevent==21.12.0 greenlet==1.1.3 ########################### @@ -189,8 +189,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.1.2 - +RUN pip install --no-index --find-links=/srv/app/wheels uwsgi==2.0.19.1 gevent==21.12.0 # Create a local user and group to run the app RUN groupadd -g 92 ckan && \ useradd -rm -d /srv/app -s /bin/bash -g ckan -u 92 ckan From f33f031726da9b7f1107799f2a455eb3c88e864e Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Thu, 20 Oct 2022 14:46:13 +0200 Subject: [PATCH 128/213] changed the if statment for uswgi proc num --- images/ckan/2.7/setup/app/start_ckan.sh | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/images/ckan/2.7/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh index 8dcc548..cf28249 100755 --- a/images/ckan/2.7/setup/app/start_ckan.sh +++ b/images/ckan/2.7/setup/app/start_ckan.sh @@ -13,12 +13,9 @@ then fi # Set the common uwsgi options -if [ ${UWSGI_PROC_NO} == "0" ] || [ ${UWSGI_PROC_NO} == "" ]; - then - echo setting USWGI proc to 2 - ${UWSGI_PROC_NO}=2 -fi -UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --paste-logger /srv/app/production.ini --lazy-apps --gevent 2000 -p ${UWSGI_PROC_NO} -L --gevent-early-monkey-patch" +echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" + +UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --paste-logger /srv/app/production.ini --lazy-apps --gevent 2000 -p ${UWSGI_PROC_NO:-2} -L --gevent-early-monkey-patch" # Run the prerun script to init CKAN and create the default admin user python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } From ece3e7c0d311e8b760418ffb81ca688e1f98fc53 Mon Sep 17 00:00:00 2001 From: "blagoja.stojkoski" Date: Wed, 26 Oct 2022 15:26:19 +0200 Subject: [PATCH 129/213] * updates images to 2.9.7 and 2.8.12 --- Readme.md | 8 ++++---- compose/2.8/.env | 2 +- .../solr/{solrconfig-2.8.11.xml => solrconfig-2.8.12.xml} | 0 .../solr/{solrconfig-2.9.6.xml => solrconfig-2.9.7.xml} | 0 compose/2.9/.ckan-env | 2 +- compose/2.9/.env | 2 +- compose/2.9/solr8/ckan_init_solr.sh | 2 +- examples/harvest/.ckan-env | 2 +- examples/harvest/.env | 2 +- examples/harvest/Dockerfile | 4 ++-- examples/s3filestore/.ckan-env | 2 +- examples/s3filestore/.env | 2 +- examples/s3filestore/Dockerfile | 4 ++-- images/ckan/2.8/Dockerfile | 4 ++-- images/ckan/2.8/Dockerfile.focal | 4 ++-- images/ckan/2.9/Dockerfile | 4 ++-- images/ckan/2.9/Dockerfile.focal | 4 ++-- 17 files changed, 24 insertions(+), 24 deletions(-) rename compose/2.8/solr/{solrconfig-2.8.11.xml => solrconfig-2.8.12.xml} (100%) rename compose/2.8/solr/{solrconfig-2.9.6.xml => solrconfig-2.9.7.xml} (100%) diff --git a/Readme.md b/Readme.md index 1af3cf7..b3da9e0 100644 --- a/Readme.md +++ b/Readme.md @@ -48,7 +48,7 @@ We recommend to use a multi-stage approach to extend the docker images that we p ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.6 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.7 as extbuild # Switch to the root user USER root @@ -64,7 +64,7 @@ RUN pip wheel --wheel-dir=/wheels git+https://github.com/acmecorp/ckanext-acme@0 ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.6 +FROM ghcr.io/keitaroinc/ckan:2.9.7 # Add the custom extensions to the plugins list ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher acme @@ -94,9 +94,9 @@ You can add scripts to CKAN custom images and copy them to the *docker-afterinit ## Build To build a CKAN image run: ```sh -docker build --tag ghcr.io/keitaroinc/ckan:2.9.6 images/ckan/2.9 +docker build --tag ghcr.io/keitaroinc/ckan:2.9.7 images/ckan/2.9 ``` -The –-tag ghcr.io/keitaroinc/ckan:2.9.6 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.6 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. +The –-tag ghcr.io/keitaroinc/ckan:2.9.7 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.7 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. ## Upload to DockerHub >*It's recommended to upload built images to DockerHub* diff --git a/compose/2.8/.env b/compose/2.8/.env index 2295d54..83b4c85 100644 --- a/compose/2.8/.env +++ b/compose/2.8/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.8.11 +CKAN_VERSION=2.8.12 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/2.8/solr/solrconfig-2.8.11.xml b/compose/2.8/solr/solrconfig-2.8.12.xml similarity index 100% rename from compose/2.8/solr/solrconfig-2.8.11.xml rename to compose/2.8/solr/solrconfig-2.8.12.xml diff --git a/compose/2.8/solr/solrconfig-2.9.6.xml b/compose/2.8/solr/solrconfig-2.9.7.xml similarity index 100% rename from compose/2.8/solr/solrconfig-2.9.6.xml rename to compose/2.8/solr/solrconfig-2.9.7.xml diff --git a/compose/2.9/.ckan-env b/compose/2.9/.ckan-env index b09867b..d0b29c0 100644 --- a/compose/2.9/.ckan-env +++ b/compose/2.9/.ckan-env @@ -32,5 +32,5 @@ CKAN__DATAPUSHER__URL=http://datapusher:8000 CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000/ # Solr configuration -CKAN_VERSION=2.9.6 +CKAN_VERSION=2.9.7 CKAN_CORE_NAME=ckan diff --git a/compose/2.9/.env b/compose/2.9/.env index 462393d..0780e0c 100644 --- a/compose/2.9/.env +++ b/compose/2.9/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.6 +CKAN_VERSION=2.9.7 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/2.9/solr8/ckan_init_solr.sh b/compose/2.9/solr8/ckan_init_solr.sh index 8483fee..8ea06c3 100755 --- a/compose/2.9/solr8/ckan_init_solr.sh +++ b/compose/2.9/solr8/ckan_init_solr.sh @@ -4,7 +4,7 @@ # Arguments are supplied via environment variables: CKAN_CORE_NAME CKAN_VERSION # Example: # CKAN_CORE_NAME=ckan -# CKAN_VERSION=2.9.6 +# CKAN_VERSION=2.9.7 set -e diff --git a/examples/harvest/.ckan-env b/examples/harvest/.ckan-env index 33bbc66..d8d32dd 100644 --- a/examples/harvest/.ckan-env +++ b/examples/harvest/.ckan-env @@ -33,5 +33,5 @@ CKAN__HARVEST__MQ__TYPE=redis CKAN__HARVEST__MQ__HOSTNAME=redis # Solr configuration -CKAN_VERSION=2.9.6 +CKAN_VERSION=2.9.7 CKAN_CORE_NAME=ckan diff --git a/examples/harvest/.env b/examples/harvest/.env index 462393d..0780e0c 100644 --- a/examples/harvest/.env +++ b/examples/harvest/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.6 +CKAN_VERSION=2.9.7 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index bcafd12..0071ba3 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.6 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.7 as extbuild # Locations and tags, please use specific tags or revisions ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest @@ -30,7 +30,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.6 +FROM ghcr.io/keitaroinc/ckan:2.9.7 LABEL maintainer="Keitaro Inc " diff --git a/examples/s3filestore/.ckan-env b/examples/s3filestore/.ckan-env index 918248a..24c883a 100644 --- a/examples/s3filestore/.ckan-env +++ b/examples/s3filestore/.ckan-env @@ -38,5 +38,5 @@ CKANEXT__S3FILESTORE__REGION_NAME=us-east-1 CKANEXT__S3FILESTORE__SIGNATURE_VERSION=s3v4 # Solr configuration -CKAN_VERSION=2.9.6 +CKAN_VERSION=2.9.7 CKAN_CORE_NAME=ckan diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env index 462393d..0780e0c 100644 --- a/examples/s3filestore/.env +++ b/examples/s3filestore/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.6 +CKAN_VERSION=2.9.7 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index 18798ef..dd030b5 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.6 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.7 as extbuild # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore @@ -20,7 +20,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.6 +FROM ghcr.io/keitaroinc/ckan:2.9.7 LABEL maintainer="Keitaro Inc " diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index c6d341a..4b66580 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.14.2 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.8.11 +ENV IMAGE_TAG=2.8.12 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.11 +ENV GIT_BRANCH=ckan-2.8.12 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index 02ee63e..2e1790f 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.8.11-focal +ENV IMAGE_TAG=2.8.12-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.8.11 +ENV GIT_BRANCH=ckan-2.8.12 # Set timezone ENV TZ=UTC diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index c0ef0c2..ac23420 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.13.7 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.6 +ENV IMAGE_TAG=2.9.7 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.6 +ENV GIT_BRANCH=ckan-2.9.7 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 3b5028c..369d7fd 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.6-focal +ENV IMAGE_TAG=2.9.7-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.6 +ENV GIT_BRANCH=ckan-2.9.7 # Set timezone ENV TZ=UTC From ab469735e317d529c1ab58f4ed2b4aed78e50a14 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Mon, 31 Oct 2022 10:49:35 +0100 Subject: [PATCH 130/213] changed if for uswgi --- images/ckan/2.8/setup/app/start_ckan.sh | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/images/ckan/2.8/setup/app/start_ckan.sh b/images/ckan/2.8/setup/app/start_ckan.sh index 8dcc548..cf28249 100755 --- a/images/ckan/2.8/setup/app/start_ckan.sh +++ b/images/ckan/2.8/setup/app/start_ckan.sh @@ -13,12 +13,9 @@ then fi # Set the common uwsgi options -if [ ${UWSGI_PROC_NO} == "0" ] || [ ${UWSGI_PROC_NO} == "" ]; - then - echo setting USWGI proc to 2 - ${UWSGI_PROC_NO}=2 -fi -UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --paste-logger /srv/app/production.ini --lazy-apps --gevent 2000 -p ${UWSGI_PROC_NO} -L --gevent-early-monkey-patch" +echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" + +UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid 92 --gid 92 --http :5000 --master --enable-threads --paste config:/srv/app/production.ini --paste-logger /srv/app/production.ini --lazy-apps --gevent 2000 -p ${UWSGI_PROC_NO:-2} -L --gevent-early-monkey-patch" # Run the prerun script to init CKAN and create the default admin user python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } From cd2af213676f7358ed96485bbf929e5cedc5aa4b Mon Sep 17 00:00:00 2001 From: Kiril Poposki Date: Tue, 27 Dec 2022 12:48:38 +0100 Subject: [PATCH 131/213] adding depends on solr for ckan container --- compose/2.8/docker-compose.yml | 1 + compose/2.9/docker-compose.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/compose/2.8/docker-compose.yml b/compose/2.8/docker-compose.yml index 5711653..18c7b36 100644 --- a/compose/2.8/docker-compose.yml +++ b/compose/2.8/docker-compose.yml @@ -15,6 +15,7 @@ services: - backend depends_on: - db + - solr ports: - "0.0.0.0:${CKAN_PORT}:5000" env_file: diff --git a/compose/2.9/docker-compose.yml b/compose/2.9/docker-compose.yml index 8eac2a7..95f693c 100644 --- a/compose/2.9/docker-compose.yml +++ b/compose/2.9/docker-compose.yml @@ -15,6 +15,7 @@ services: - backend depends_on: - db + - solr ports: - "0.0.0.0:${CKAN_PORT}:5000" env_file: From 5505ee136416a23b97185ef2b0068399b261ca20 Mon Sep 17 00:00:00 2001 From: stojanovskis1 Date: Wed, 18 Jan 2023 14:49:08 +0100 Subject: [PATCH 132/213] changed start-ckan.sh, setting secrets in ini file --- images/ckan/2.7/setup/app/start_ckan.sh | 8 ++++++++ images/ckan/2.8/setup/app/start_ckan.sh | 8 ++++++++ images/ckan/2.9/setup/app/start_ckan.sh | 8 ++++++++ 3 files changed, 24 insertions(+) diff --git a/images/ckan/2.7/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh index cf28249..d8dac90 100755 --- a/images/ckan/2.7/setup/app/start_ckan.sh +++ b/images/ckan/2.7/setup/app/start_ckan.sh @@ -12,6 +12,14 @@ then done fi +if grep -E "beaker.session.secret ?= ?$" ckan.ini +then + echo "Setting secrets in ini file" + ckan config-tool $CKAN_INI "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" + ckan config-tool $CKAN_INI "api_token.jwt.encode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" + ckan config-tool $CKAN_INI "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" +fi + # Set the common uwsgi options echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" diff --git a/images/ckan/2.8/setup/app/start_ckan.sh b/images/ckan/2.8/setup/app/start_ckan.sh index cf28249..d8dac90 100755 --- a/images/ckan/2.8/setup/app/start_ckan.sh +++ b/images/ckan/2.8/setup/app/start_ckan.sh @@ -12,6 +12,14 @@ then done fi +if grep -E "beaker.session.secret ?= ?$" ckan.ini +then + echo "Setting secrets in ini file" + ckan config-tool $CKAN_INI "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" + ckan config-tool $CKAN_INI "api_token.jwt.encode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" + ckan config-tool $CKAN_INI "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" +fi + # Set the common uwsgi options echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index f1b509f..2210987 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -12,6 +12,14 @@ then done fi +if grep -E "beaker.session.secret ?= ?$" ckan.ini +then + echo "Setting secrets in ini file" + ckan config-tool $CKAN_INI "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" + ckan config-tool $CKAN_INI "api_token.jwt.encode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" + ckan config-tool $CKAN_INI "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" +fi + echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid ckan --gid ckan --http :5000 --master --enable-threads --wsgi-file /srv/app/wsgi.py --module wsgi:application --lazy-apps --gevent 2000 -p ${UWSGI_PROC_NO:-2} -L --gevent-early-monkey-patch --vacuum --harakiri 50 --callable application" From acd253c403625129ed8b13819316080de297e024 Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Mon, 23 Jan 2023 10:33:00 +0100 Subject: [PATCH 133/213] changed ckan.ini value, added placeholders in .ckan-env file --- compose/2.8/.ckan-env | 4 ++++ compose/2.9/.ckan-env | 4 ++++ images/ckan/2.7/Dockerfile | 1 + images/ckan/2.7/setup/app/start_ckan.sh | 8 +++----- images/ckan/2.8/Dockerfile | 1 + images/ckan/2.8/setup/app/start_ckan.sh | 8 +++----- images/ckan/2.9/Dockerfile | 1 + images/ckan/2.9/setup/app/start_ckan.sh | 2 +- 8 files changed, 18 insertions(+), 11 deletions(-) diff --git a/compose/2.8/.ckan-env b/compose/2.8/.ckan-env index f67c88b..6e37970 100644 --- a/compose/2.8/.ckan-env +++ b/compose/2.8/.ckan-env @@ -10,6 +10,10 @@ CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 CKAN_MAX_UPLOAD_SIZE_MB=10 +CKAN___BEAKER__SESSION__SECRET=CHANGE_ME +# See https://docs.ckan.org/en/latest/maintaining/configuration.html#api-token-settings +CKAN___API_TOKEN__JWT__ENCODE__SECRET=string:CHANGE_ME +CKAN___API_TOKEN__JWT__DECODE__SECRET=string:CHANGE_ME # CKAN Plugins CKAN__PLUGINS=envvars image_view text_view recline_view datastore datapusher # CKAN requires storage path to be set in order for filestore to be enabled diff --git a/compose/2.9/.ckan-env b/compose/2.9/.ckan-env index d0b29c0..9f7e329 100644 --- a/compose/2.9/.ckan-env +++ b/compose/2.9/.ckan-env @@ -10,6 +10,10 @@ CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 CKAN_MAX_UPLOAD_SIZE_MB=10 +CKAN___BEAKER__SESSION__SECRET=CHANGE_ME +# See https://docs.ckan.org/en/latest/maintaining/configuration.html#api-token-settings +CKAN___API_TOKEN__JWT__ENCODE__SECRET=string:CHANGE_ME +CKAN___API_TOKEN__JWT__DECODE__SECRET=string:CHANGE_ME # CKAN Plugins CKAN__PLUGINS=envvars image_view text_view recline_view datastore datapusher # CKAN requires storage path to be set in order for filestore to be enabled diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 9578240..2464781 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -158,6 +158,7 @@ RUN pip install -e /srv/app/src/ckan && \ echo "UTC" > /etc/timezone && \ # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ + paster --plugin=ckan config-tool ${CKAN_INI} "beaker.session.secret = " && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Set the default level for extensions to INFO paster --plugin=ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ diff --git a/images/ckan/2.7/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh index d8dac90..73b5bf3 100755 --- a/images/ckan/2.7/setup/app/start_ckan.sh +++ b/images/ckan/2.7/setup/app/start_ckan.sh @@ -12,12 +12,10 @@ then done fi -if grep -E "beaker.session.secret ?= ?$" ckan.ini +if grep -E "beaker.session.secret ?= ?$" production.ini then - echo "Setting secrets in ini file" - ckan config-tool $CKAN_INI "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" - ckan config-tool $CKAN_INI "api_token.jwt.encode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" - ckan config-tool $CKAN_INI "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" + echo "Setting beaker.session.secret in ini file" + paster --plugin=ckan config-tool $CKAN_INI "beaker.session.secret=$(python -c 'import secrets; print(secrets.token_urlsafe())')" fi # Set the common uwsgi options diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 4b66580..332939e 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -151,6 +151,7 @@ RUN pip install -e /srv/app/src/ckan && \ echo "UTC" > /etc/timezone && \ # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ + paster --plugin=ckan config-tool ${CKAN_INI} "beaker.session.secret = " && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Set the default level for extensions to INFO paster --plugin=ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ diff --git a/images/ckan/2.8/setup/app/start_ckan.sh b/images/ckan/2.8/setup/app/start_ckan.sh index d8dac90..73b5bf3 100755 --- a/images/ckan/2.8/setup/app/start_ckan.sh +++ b/images/ckan/2.8/setup/app/start_ckan.sh @@ -12,12 +12,10 @@ then done fi -if grep -E "beaker.session.secret ?= ?$" ckan.ini +if grep -E "beaker.session.secret ?= ?$" production.ini then - echo "Setting secrets in ini file" - ckan config-tool $CKAN_INI "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" - ckan config-tool $CKAN_INI "api_token.jwt.encode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" - ckan config-tool $CKAN_INI "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" + echo "Setting beaker.session.secret in ini file" + paster --plugin=ckan config-tool $CKAN_INI "beaker.session.secret=$(python -c 'import secrets; print(secrets.token_urlsafe())')" fi # Set the common uwsgi options diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index ac23420..8a22e6c 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -162,6 +162,7 @@ RUN pip install -e /srv/app/src/ckan && \ echo "UTC" > /etc/timezone && \ # Generate CKAN config ckan generate config ${APP_DIR}/production.ini && \ + ckan config-tool ${CKAN_INI} "beaker.session.secret = " && \ # Configure plugins ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Create the data directory diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index 2210987..bdb347c 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -12,7 +12,7 @@ then done fi -if grep -E "beaker.session.secret ?= ?$" ckan.ini +if grep -E "beaker.session.secret ?= ?$" production.ini then echo "Setting secrets in ini file" ckan config-tool $CKAN_INI "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" From 4a9854e3b5cd48881d0f4d7a30ee2f0a3ec805cc Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Mon, 23 Jan 2023 12:21:49 +0100 Subject: [PATCH 134/213] changed production to ckan.ini --- images/ckan/2.7/setup/app/start_ckan.sh | 2 +- images/ckan/2.8/setup/app/start_ckan.sh | 2 +- images/ckan/2.9/setup/app/start_ckan.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/images/ckan/2.7/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh index 73b5bf3..e8584bd 100755 --- a/images/ckan/2.7/setup/app/start_ckan.sh +++ b/images/ckan/2.7/setup/app/start_ckan.sh @@ -12,7 +12,7 @@ then done fi -if grep -E "beaker.session.secret ?= ?$" production.ini +if grep -E "beaker.session.secret ?= ?$" ckan.ini then echo "Setting beaker.session.secret in ini file" paster --plugin=ckan config-tool $CKAN_INI "beaker.session.secret=$(python -c 'import secrets; print(secrets.token_urlsafe())')" diff --git a/images/ckan/2.8/setup/app/start_ckan.sh b/images/ckan/2.8/setup/app/start_ckan.sh index 73b5bf3..e8584bd 100755 --- a/images/ckan/2.8/setup/app/start_ckan.sh +++ b/images/ckan/2.8/setup/app/start_ckan.sh @@ -12,7 +12,7 @@ then done fi -if grep -E "beaker.session.secret ?= ?$" production.ini +if grep -E "beaker.session.secret ?= ?$" ckan.ini then echo "Setting beaker.session.secret in ini file" paster --plugin=ckan config-tool $CKAN_INI "beaker.session.secret=$(python -c 'import secrets; print(secrets.token_urlsafe())')" diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index bdb347c..2210987 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -12,7 +12,7 @@ then done fi -if grep -E "beaker.session.secret ?= ?$" production.ini +if grep -E "beaker.session.secret ?= ?$" ckan.ini then echo "Setting secrets in ini file" ckan config-tool $CKAN_INI "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" From 24d7858815da1a4be19bb169c9499e8593ed4cd0 Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Mon, 23 Jan 2023 12:39:57 +0100 Subject: [PATCH 135/213] changed CKAN_INI to APP_DIR --- images/ckan/2.7/Dockerfile | 2 +- images/ckan/2.7/setup/app/start_ckan.sh | 4 ++-- images/ckan/2.8/Dockerfile | 2 +- images/ckan/2.8/setup/app/start_ckan.sh | 4 ++-- images/ckan/2.9/Dockerfile | 2 +- images/ckan/2.9/setup/app/start_ckan.sh | 8 ++++---- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 2464781..4f2fc2d 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -158,7 +158,7 @@ RUN pip install -e /srv/app/src/ckan && \ echo "UTC" > /etc/timezone && \ # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ - paster --plugin=ckan config-tool ${CKAN_INI} "beaker.session.secret = " && \ + paster --plugin=ckan config-tool ${APP_DIR} "beaker.session.secret = " && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Set the default level for extensions to INFO paster --plugin=ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ diff --git a/images/ckan/2.7/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh index e8584bd..b0227c7 100755 --- a/images/ckan/2.7/setup/app/start_ckan.sh +++ b/images/ckan/2.7/setup/app/start_ckan.sh @@ -12,10 +12,10 @@ then done fi -if grep -E "beaker.session.secret ?= ?$" ckan.ini +if grep -E "beaker.session.secret ?= ?$" production.ini then echo "Setting beaker.session.secret in ini file" - paster --plugin=ckan config-tool $CKAN_INI "beaker.session.secret=$(python -c 'import secrets; print(secrets.token_urlsafe())')" + paster --plugin=ckan config-tool $APP_DIR "beaker.session.secret=$(python -c 'import secrets; print(secrets.token_urlsafe())')" fi # Set the common uwsgi options diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 332939e..061173d 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -151,7 +151,7 @@ RUN pip install -e /srv/app/src/ckan && \ echo "UTC" > /etc/timezone && \ # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ - paster --plugin=ckan config-tool ${CKAN_INI} "beaker.session.secret = " && \ + paster --plugin=ckan config-tool ${APP_DIR} "beaker.session.secret = " && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Set the default level for extensions to INFO paster --plugin=ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ diff --git a/images/ckan/2.8/setup/app/start_ckan.sh b/images/ckan/2.8/setup/app/start_ckan.sh index e8584bd..b0227c7 100755 --- a/images/ckan/2.8/setup/app/start_ckan.sh +++ b/images/ckan/2.8/setup/app/start_ckan.sh @@ -12,10 +12,10 @@ then done fi -if grep -E "beaker.session.secret ?= ?$" ckan.ini +if grep -E "beaker.session.secret ?= ?$" production.ini then echo "Setting beaker.session.secret in ini file" - paster --plugin=ckan config-tool $CKAN_INI "beaker.session.secret=$(python -c 'import secrets; print(secrets.token_urlsafe())')" + paster --plugin=ckan config-tool $APP_DIR "beaker.session.secret=$(python -c 'import secrets; print(secrets.token_urlsafe())')" fi # Set the common uwsgi options diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 8a22e6c..7a3679b 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -162,7 +162,7 @@ RUN pip install -e /srv/app/src/ckan && \ echo "UTC" > /etc/timezone && \ # Generate CKAN config ckan generate config ${APP_DIR}/production.ini && \ - ckan config-tool ${CKAN_INI} "beaker.session.secret = " && \ + ckan config-tool ${APP_DIR} "beaker.session.secret = " && \ # Configure plugins ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Create the data directory diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index 2210987..17ec157 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -12,12 +12,12 @@ then done fi -if grep -E "beaker.session.secret ?= ?$" ckan.ini +if grep -E "beaker.session.secret ?= ?$" production.ini then echo "Setting secrets in ini file" - ckan config-tool $CKAN_INI "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" - ckan config-tool $CKAN_INI "api_token.jwt.encode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" - ckan config-tool $CKAN_INI "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" + ckan config-tool $APP_DIR "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" + ckan config-tool $APP_DIR "api_token.jwt.encode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" + ckan config-tool $APP_DIR "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" fi echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" From 6c052e947571c6151e128edc056ae449c02d440e Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Mon, 23 Jan 2023 13:11:38 +0100 Subject: [PATCH 136/213] added production.ini to APP_DIR in Dockerfile --- images/ckan/2.7/Dockerfile | 2 +- images/ckan/2.8/Dockerfile | 2 +- images/ckan/2.9/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 4f2fc2d..5974c69 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -158,7 +158,7 @@ RUN pip install -e /srv/app/src/ckan && \ echo "UTC" > /etc/timezone && \ # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ - paster --plugin=ckan config-tool ${APP_DIR} "beaker.session.secret = " && \ + paster --plugin=ckan config-tool ${APP_DIR}/production.ini "beaker.session.secret = " && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Set the default level for extensions to INFO paster --plugin=ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index 061173d..ea24952 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -151,7 +151,7 @@ RUN pip install -e /srv/app/src/ckan && \ echo "UTC" > /etc/timezone && \ # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ - paster --plugin=ckan config-tool ${APP_DIR} "beaker.session.secret = " && \ + paster --plugin=ckan config-tool ${APP_DIR}/production.ini "beaker.session.secret = " && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Set the default level for extensions to INFO paster --plugin=ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 7a3679b..aba8ebb 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -162,7 +162,7 @@ RUN pip install -e /srv/app/src/ckan && \ echo "UTC" > /etc/timezone && \ # Generate CKAN config ckan generate config ${APP_DIR}/production.ini && \ - ckan config-tool ${APP_DIR} "beaker.session.secret = " && \ + ckan config-tool ${APP_DIR}/production.ini "beaker.session.secret = " && \ # Configure plugins ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Create the data directory From 04743236184107166ea7726506f30522042e2f8b Mon Sep 17 00:00:00 2001 From: blagoja Date: Wed, 25 Jan 2023 16:49:23 +0100 Subject: [PATCH 137/213] Change the version of the postgres for CKAN 2.8 --- compose/2.8/postgresql/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/2.8/postgresql/Dockerfile b/compose/2.8/postgresql/Dockerfile index e5fcbb7..95e8cf6 100644 --- a/compose/2.8/postgresql/Dockerfile +++ b/compose/2.8/postgresql/Dockerfile @@ -1,4 +1,4 @@ -FROM postgis/postgis:14-3.2-alpine +FROM postgis/postgis:11-3.3-alpine # Allow connections; we don't map out any ports so only linked docker containers can connect RUN echo "host all all 0.0.0.0/0 md5" >> /var/lib/postgresql/data/pg_hba.conf From eeef56192264b55440f9c97a925f5414e08557d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Bi=C4=8Di=C5=A1t=C4=9B?= Date: Mon, 6 Feb 2023 10:51:28 +0100 Subject: [PATCH 138/213] changed start-ckan.sh, setting secrets in ini file --- images/ckan/2.7/setup/app/start_ckan.sh | 4 ++-- images/ckan/2.8/setup/app/start_ckan.sh | 4 ++-- images/ckan/2.9/setup/app/start_ckan.sh | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/images/ckan/2.7/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh index b0227c7..a7bf12a 100755 --- a/images/ckan/2.7/setup/app/start_ckan.sh +++ b/images/ckan/2.7/setup/app/start_ckan.sh @@ -12,10 +12,10 @@ then done fi -if grep -E "beaker.session.secret ?= ?$" production.ini +if grep -E "beaker.session.secret ?= ?$" $APP_DIR/production.ini then echo "Setting beaker.session.secret in ini file" - paster --plugin=ckan config-tool $APP_DIR "beaker.session.secret=$(python -c 'import secrets; print(secrets.token_urlsafe())')" + paster --plugin=ckan config-tool $APP_DIR/production.ini "beaker.session.secret=$(python -c 'import secrets; print(secrets.token_urlsafe())')" fi # Set the common uwsgi options diff --git a/images/ckan/2.8/setup/app/start_ckan.sh b/images/ckan/2.8/setup/app/start_ckan.sh index b0227c7..a7bf12a 100755 --- a/images/ckan/2.8/setup/app/start_ckan.sh +++ b/images/ckan/2.8/setup/app/start_ckan.sh @@ -12,10 +12,10 @@ then done fi -if grep -E "beaker.session.secret ?= ?$" production.ini +if grep -E "beaker.session.secret ?= ?$" $APP_DIR/production.ini then echo "Setting beaker.session.secret in ini file" - paster --plugin=ckan config-tool $APP_DIR "beaker.session.secret=$(python -c 'import secrets; print(secrets.token_urlsafe())')" + paster --plugin=ckan config-tool $APP_DIR/production.ini "beaker.session.secret=$(python -c 'import secrets; print(secrets.token_urlsafe())')" fi # Set the common uwsgi options diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index 17ec157..f1b5986 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -12,12 +12,12 @@ then done fi -if grep -E "beaker.session.secret ?= ?$" production.ini +if grep -E "beaker.session.secret ?= ?$" $APP_DIR/production.ini then echo "Setting secrets in ini file" - ckan config-tool $APP_DIR "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" - ckan config-tool $APP_DIR "api_token.jwt.encode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" - ckan config-tool $APP_DIR "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" + ckan config-tool $APP_DIR/production.ini "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" + ckan config-tool $APP_DIR/production.ini "api_token.jwt.encode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" + ckan config-tool $APP_DIR/production.ini "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" fi echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" From 0335a84b5166b11bc162e9c70c60b9386145aefa Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Thu, 9 Feb 2023 13:38:00 +0100 Subject: [PATCH 139/213] added beaker-session-secret --- images/ckan/2.7/Dockerfile.focal | 1 + images/ckan/2.8/Dockerfile.focal | 1 + images/ckan/2.9/Dockerfile.focal | 1 + 3 files changed, 3 insertions(+) diff --git a/images/ckan/2.7/Dockerfile.focal b/images/ckan/2.7/Dockerfile.focal index 5e0cfbc..4bd14db 100644 --- a/images/ckan/2.7/Dockerfile.focal +++ b/images/ckan/2.7/Dockerfile.focal @@ -205,6 +205,7 @@ RUN pip install -e /srv/app/src/ckan && \ # Create and update CKAN config # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ + paster --plugin=ckan config-tool ${APP_DIR}/production.ini "beaker.session.secret = " && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Set the default level for extensions to INFO paster --plugin=ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ diff --git a/images/ckan/2.8/Dockerfile.focal b/images/ckan/2.8/Dockerfile.focal index 2e1790f..1abcc72 100644 --- a/images/ckan/2.8/Dockerfile.focal +++ b/images/ckan/2.8/Dockerfile.focal @@ -191,6 +191,7 @@ RUN pip install -e /srv/app/src/ckan && \ # Create and update CKAN config # Generate CKAN config paster --plugin=ckan make-config ckan ${APP_DIR}/production.ini && \ + paster --plugin=ckan config-tool ${APP_DIR}/production.ini "beaker.session.secret = " && \ paster --plugin=ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Set the default level for extensions to INFO paster --plugin=ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 369d7fd..6e68751 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -210,6 +210,7 @@ RUN pip install -e /srv/app/src/ckan && \ # Generate CKAN config ckan generate config ${APP_DIR}/production.ini && \ # Configure plugins + ckan config-tool ${APP_DIR}/production.ini "beaker.session.secret = " && \ ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ # Create the data directory mkdir ${DATA_DIR} && \ From 5b8a2358fa3728d1edd1aa3e8c6a9ed5752df8c3 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Thu, 16 Feb 2023 16:42:29 +0100 Subject: [PATCH 140/213] created ckan2.10 image for alpine --- images/ckan/2.10/Dockerfile | 190 ++++++++++++++ images/ckan/2.10/Dockerfile.focal | 243 ++++++++++++++++++ images/ckan/2.10/patches/.gitkeep | 0 .../ckan/2.10/scripts/apply_ckan_patches.sh | 5 + images/ckan/2.10/setup/app/extra_scripts.sh | 4 + .../api/3/action/status_show/index.html | 25 ++ .../2.10/setup/app/maintenance/index.html | 25 ++ .../ckan/2.10/setup/app/maintenance/serve.py | 35 +++ images/ckan/2.10/setup/app/prerun.py | 230 +++++++++++++++++ images/ckan/2.10/setup/app/start_ckan.sh | 66 +++++ images/ckan/2.10/setup/app/uwsgi.conf | 2 + images/ckan/2.10/setup/app/wsgi.py | 28 ++ 12 files changed, 853 insertions(+) create mode 100644 images/ckan/2.10/Dockerfile create mode 100644 images/ckan/2.10/Dockerfile.focal create mode 100644 images/ckan/2.10/patches/.gitkeep create mode 100755 images/ckan/2.10/scripts/apply_ckan_patches.sh create mode 100755 images/ckan/2.10/setup/app/extra_scripts.sh create mode 100644 images/ckan/2.10/setup/app/maintenance/api/3/action/status_show/index.html create mode 100644 images/ckan/2.10/setup/app/maintenance/index.html create mode 100644 images/ckan/2.10/setup/app/maintenance/serve.py create mode 100644 images/ckan/2.10/setup/app/prerun.py create mode 100755 images/ckan/2.10/setup/app/start_ckan.sh create mode 100644 images/ckan/2.10/setup/app/uwsgi.conf create mode 100644 images/ckan/2.10/setup/app/wsgi.py diff --git a/images/ckan/2.10/Dockerfile b/images/ckan/2.10/Dockerfile new file mode 100644 index 0000000..becda88 --- /dev/null +++ b/images/ckan/2.10/Dockerfile @@ -0,0 +1,190 @@ +################## +### Build CKAN ### +################## +FROM alpine:3.17.2 as ckanbuild + +# Used by Github Actions to tag the image with +ENV IMAGE_TAG=2.10.0 + +# Set CKAN version to build +ENV GIT_URL=https://github.com/ckan/ckan.git +ENV GIT_BRANCH=ckan-2.10.0 + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +WORKDIR ${SRC_DIR} + +# Packages to build CKAN requirements and plugins +RUN apk add --no-cache \ + python3 \ + python3-dev \ + git \ + curl \ + postgresql-dev \ + linux-headers \ + gcc \ + make \ + g++ \ + autoconf \ + automake \ + libtool \ + patch \ + musl-dev \ + pcre-dev \ + pcre \ + libffi-dev \ + libxml2-dev \ + libxslt-dev + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Downgrade setuptools so that CKAN requirements can be built +RUN pip install setuptools==44.1.0 + +# Fetch and build CKAN and requirements +RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan +# Copy patches and apply patches script +COPY ./patches ${SRC_DIR}/patches +COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh +# Apply patches +# RUN ${SRC_DIR}/apply_ckan_patches.sh +RUN rm -rf /srv/app/src/ckan/.git +RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt +RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.12.0 greenlet==1.1.3 + + +########################### +### Default-Extensions #### +########################### +FROM alpine:3.17.2 as extbuild + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +# List of default extensions +ENV DEFAULT_EXTENSIONS envvars + +# Locations and tags, please use specific tags or revisions +ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars +ENV ENVVARS_GIT_BRANCH=0.0.2 + +RUN apk add --no-cache \ + python3 \ + python3-dev \ + git \ + curl + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Fetch and build the default CKAN extensions +RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars + +############ +### MAIN ### +############ +FROM alpine:3.17.2 + +LABEL maintainer="Keitaro Inc " +LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan + +ENV APP_DIR=/srv/app +ENV SRC_DIR=/srv/app/src +ENV CKAN_DIR=${SRC_DIR}/ckan +ENV DATA_DIR=/srv/app/data +ENV PIP_SRC=${SRC_DIR} +ENV CKAN_SITE_URL=http://localhost:5000 +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher + +# Install necessary packages to run CKAN +RUN apk add --no-cache \ + python3 \ + bash \ + git \ + gettext \ + curl \ + postgresql-client \ + libmagic \ + pcre \ + libxslt \ + libxml2 \ + tzdata \ + apache2-utils && \ + # Create SRC_DIR + mkdir -p ${SRC_DIR} + + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Get artifacts from build stages +COPY --from=ckanbuild /wheels /srv/app/wheels +COPY --from=extbuild /wheels /srv/app/ext_wheels +COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} + +# Additional install steps for build stages artifacts +RUN pip install --no-index --find-links=/srv/app/wheels uWSGI==2.0.20 gevent==21.12.0 + +# Create a local user and group to run the app +RUN addgroup -g 92 -S ckan && \ + adduser -u 92 -h /srv/app -H -D -S -G ckan ckan + +WORKDIR ${CKAN_DIR} + +# Install CKAN +RUN pip install -e /srv/app/src/ckan && \ + cp who.ini ${APP_DIR} && \ + pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ + # Install default CKAN extensions + pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars && \ + # Create and update CKAN config + # Set timezone + echo "UTC" > /etc/timezone && \ + # Generate CKAN config + ckan generate config ${APP_DIR}/production.ini && \ + ckan config-tool ${APP_DIR}/production.ini "beaker.session.secret = " && \ + # Configure plugins + ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + # Create the data directory + mkdir ${DATA_DIR} && \ + # Webassets can't be loaded from env variables at runtime, it needs to be in the config so that it is created + ckan config-tool ${APP_DIR}/production.ini "ckan.webassets.path = ${DATA_DIR}/webassets" && \ + # Set the default level for extensions to INFO + ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ + # Change ownership to app user + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/wheels /srv/app/ext_wheels + +# Copy necessary scripts +COPY setup/app ${APP_DIR} + +WORKDIR ${APP_DIR} + +# Create entrypoint directory for children image scripts +ONBUILD RUN mkdir docker-entrypoint.d + +# Create afterinit directory for children image scripts +ONBUILD RUN mkdir docker-afterinit.d + +EXPOSE 5000 + +HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 + +USER ckan + +CMD ["/srv/app/start_ckan.sh"] diff --git a/images/ckan/2.10/Dockerfile.focal b/images/ckan/2.10/Dockerfile.focal new file mode 100644 index 0000000..369d7fd --- /dev/null +++ b/images/ckan/2.10/Dockerfile.focal @@ -0,0 +1,243 @@ +################## +### Build CKAN ### +################## +FROM ubuntu:focal-20210827 as ckanbuild + +# Used by Github Actions to tag the image with +ENV IMAGE_TAG=2.9.7-focal + +# Set CKAN version to build +ENV GIT_URL=https://github.com/ckan/ckan.git +ENV GIT_BRANCH=ckan-2.9.7 + +# Set timezone +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Set Locale +ENV LC_ALL=en_US.UTF-8 + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +WORKDIR ${SRC_DIR} + +# Set the locale +RUN apt-get update +RUN apt-get install --no-install-recommends -y locales +RUN sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen +RUN dpkg-reconfigure --frontend=noninteractive locales +RUN update-locale LANG=${LC_ALL} + +# Instal apt-utils +RUN apt-get install --no-install-recommends -y \ + apt-utils + +# Packages to build CKAN requirements and plugins +RUN apt-get install --no-install-recommends -y \ + git \ + curl \ + ca-certificates \ + python3 \ + libpq-dev \ + linux-headers-generic \ + gcc-10 \ + make \ + g++-10 \ + autoconf \ + automake \ + libtool \ + patch \ + libpcre3-dev \ + libpcre3 \ + python3-dev \ + libffi-dev \ + libxml2-dev \ + libxslt-dev + +# Use gcc 10 +RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10 --slave /usr/bin/x86_64-linux-gnu-gcc x86_64-linux-gnu-gcc /usr/bin/x86_64-linux-gnu-gcc-10 + +# Link python to python3 +RUN ln -s /usr/bin/python3 /usr/bin/python + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Downgrade setuptools so that CKAN requirements can be built +RUN pip install setuptools==44.1.0 + +# Fetch and build CKAN and requirements +RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan +# Copy patches and apply patches script +COPY ./patches ${SRC_DIR}/patches +COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh +# Apply patches +RUN ${SRC_DIR}/apply_ckan_patches.sh +RUN rm -rf /srv/app/src/ckan/.git +RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt +RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.12.0 greenlet==1.1.3 + + +########################### +### Default-Extensions #### +########################### +FROM ubuntu:focal-20210827 as extbuild + +# Set src dirs +ENV SRC_DIR=/srv/app/src +ENV PIP_SRC=${SRC_DIR} + +# List of default extensions +ENV DEFAULT_EXTENSIONS envvars + +# Locations and tags, please use specific tags or revisions +ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars +ENV ENVVARS_GIT_BRANCH=0.0.1 + +RUN apt-get update && \ + apt-get install --no-install-recommends -y \ + git \ + curl \ + ca-certificates \ + python3 \ + python3-dev + +# Link python to python3 +RUN ln -s /usr/bin/python3 /usr/bin/python + +# Create the src directory +RUN mkdir -p ${SRC_DIR} + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Downgrade setuptools so that CKAN requirements can be built +RUN pip install setuptools==44.1.0 + +# Fetch and build the default CKAN extensions +RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#egg=ckanext-envvars + +############ +### MAIN ### +############ +FROM ubuntu:focal-20210827 + +LABEL maintainer="Keitaro Inc " +LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan + +# Set timezone +ENV TZ=UTC +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Set Locale +ENV LC_ALL=en_US.UTF-8 + +# Set the locale +RUN apt-get update && \ + apt-get install --no-install-recommends -y locales && \ + sed -i "/$LC_ALL/s/^# //g" /etc/locale.gen && \ + dpkg-reconfigure --frontend=noninteractive locales && \ + update-locale LANG=${LC_ALL} && \ + rm -rf /var/lib/apt/lists/* + +ENV APP_DIR=/srv/app +ENV SRC_DIR=/srv/app/src +ENV CKAN_DIR=${SRC_DIR}/ckan +ENV DATA_DIR=/srv/app/data +ENV PIP_SRC=${SRC_DIR} +ENV CKAN_SITE_URL=http://localhost:5000 +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher + +# Install necessary packages to run CKAN +RUN apt-get update && \ + apt-get install --no-install-recommends -y \ + gettext \ + curl \ + ca-certificates \ + libpq5 \ + git \ + postgresql-client \ + python3 \ + python3-distutils \ + libpython3.8 \ + libmagic1 \ + libpcre3 \ + libxslt1.1 \ + libxml2 \ + tzdata \ + apache2-utils && \ + rm -rf /var/lib/apt/lists/* && \ + # Create SRC_DIR + mkdir -p ${SRC_DIR} && \ + # Link python to python3 + ln -s /usr/bin/python3 /usr/bin/python + +# Install pip +RUN curl -o ${SRC_DIR}/get-pip.py https://bootstrap.pypa.io/get-pip.py && \ + python ${SRC_DIR}/get-pip.py + +# Downgrade setuptools so that CKAN requirements can be built +RUN pip install setuptools==44.1.0 + +# Get artifacts from build stages +COPY --from=ckanbuild /wheels /srv/app/wheels +COPY --from=extbuild /wheels /srv/app/ext_wheels +COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} + +# Additional install steps for build stages artifacts +RUN pip install --no-index --find-links=/srv/app/wheels uWSGI==2.0.20 gevent==21.12.0 + +# Create a local user and group to run the app +RUN groupadd -g 92 ckan && \ + useradd -rm -d /srv/app -s /bin/bash -g ckan -u 92 ckan + +WORKDIR ${CKAN_DIR} + +# Install CKAN +RUN pip install -e /srv/app/src/ckan && \ + cp who.ini ${APP_DIR} && \ + pip install --no-index --find-links=/srv/app/wheels -r requirements.txt && \ + # Install default CKAN extensions + pip install --no-index --find-links=/srv/app/ext_wheels ckanext-envvars && \ + # Create and update CKAN config + # Generate CKAN config + ckan generate config ${APP_DIR}/production.ini && \ + # Configure plugins + ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + # Create the data directory + mkdir ${DATA_DIR} && \ + # Webassets can't be loaded from env variables at runtime, it needs to be in the config so that it is created + ckan config-tool ${APP_DIR}/production.ini "ckan.webassets.path = ${DATA_DIR}/webassets" && \ + # Set the default level for extensions to INFO + ckan config-tool ${APP_DIR}/production.ini -s logger_ckanext -e level=INFO && \ + # Change ownership to app user + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/wheels /srv/app/ext_wheels + +# Copy necessary scripts +COPY setup/app ${APP_DIR} + +WORKDIR ${APP_DIR} + +# Create entrypoint directory for children image scripts +ONBUILD RUN mkdir docker-entrypoint.d + +# Create afterinit directory for children image scripts +ONBUILD RUN mkdir docker-afterinit.d + +EXPOSE 5000 + +HEALTHCHECK --interval=10s --timeout=5s --retries=5 CMD curl --fail http://localhost:5000/api/3/action/status_show || exit 1 + +USER ckan + +CMD ["/srv/app/start_ckan.sh"] diff --git a/images/ckan/2.10/patches/.gitkeep b/images/ckan/2.10/patches/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/images/ckan/2.10/scripts/apply_ckan_patches.sh b/images/ckan/2.10/scripts/apply_ckan_patches.sh new file mode 100755 index 0000000..a7bceb9 --- /dev/null +++ b/images/ckan/2.10/scripts/apply_ckan_patches.sh @@ -0,0 +1,5 @@ +#!/bin/bash +shopt -s nullglob +for patch in patches/*.patch; do + /usr/bin/patch -p0 -i $patch +done diff --git a/images/ckan/2.10/setup/app/extra_scripts.sh b/images/ckan/2.10/setup/app/extra_scripts.sh new file mode 100755 index 0000000..80a70ef --- /dev/null +++ b/images/ckan/2.10/setup/app/extra_scripts.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# this is called before uwsgi is executed +# uset his to add extra scripts before ckan is started diff --git a/images/ckan/2.10/setup/app/maintenance/api/3/action/status_show/index.html b/images/ckan/2.10/setup/app/maintenance/api/3/action/status_show/index.html new file mode 100644 index 0000000..50276ec --- /dev/null +++ b/images/ckan/2.10/setup/app/maintenance/api/3/action/status_show/index.html @@ -0,0 +1,25 @@ + + + + + Maintenance + + +

Maintenance

+

Our data portal is currently in maintenance, please try in a while.

+ + diff --git a/images/ckan/2.10/setup/app/maintenance/index.html b/images/ckan/2.10/setup/app/maintenance/index.html new file mode 100644 index 0000000..50276ec --- /dev/null +++ b/images/ckan/2.10/setup/app/maintenance/index.html @@ -0,0 +1,25 @@ + + + + + Maintenance + + +

Maintenance

+

Our data portal is currently in maintenance, please try in a while.

+ + diff --git a/images/ckan/2.10/setup/app/maintenance/serve.py b/images/ckan/2.10/setup/app/maintenance/serve.py new file mode 100644 index 0000000..77f3bd4 --- /dev/null +++ b/images/ckan/2.10/setup/app/maintenance/serve.py @@ -0,0 +1,35 @@ +""" +Copyright (c) 2016 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler +import os + +PORT = 5000 + +web_dir = os.path.join(os.path.dirname(__file__)) +os.chdir(web_dir) + + +def run(server_class=ThreadingHTTPServer, handler_class=SimpleHTTPRequestHandler): + server_address = ("0.0.0.0", PORT) + httpd = server_class(server_address, handler_class) + print("Starting maintenance mode") + httpd.serve_forever() + + +if __name__ == "__main__": + run() + diff --git a/images/ckan/2.10/setup/app/prerun.py b/images/ckan/2.10/setup/app/prerun.py new file mode 100644 index 0000000..64a56bc --- /dev/null +++ b/images/ckan/2.10/setup/app/prerun.py @@ -0,0 +1,230 @@ +""" +Copyright (c) 2016 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import os +import sys +import subprocess +import psycopg2 +from sqlalchemy.engine.url import make_url +import urllib.request, urllib.error, urllib.parse +import re +import json + +import time + +ckan_ini = os.environ.get('CKAN_INI', '/srv/app/production.ini') + +RETRY = 5 + +def check_db_connection(retry=None): + + print('[prerun] Start check_db_connection...') + + if retry is None: + retry = RETRY + elif retry == 0: + print('[prerun] Giving up after 5 tries...') + sys.exit(1) + + conn_str = os.environ.get('CKAN_SQLALCHEMY_URL', '') + try: + db_user = make_url(conn_str).username + db_passwd = make_url(conn_str).password + db_host = make_url(conn_str).host + db_name = make_url(conn_str).database + connection = psycopg2.connect(user=db_user, + host=db_host, + password=db_passwd, + database=db_name) + + except psycopg2.Error as e: + print((str(e))) + print('[prerun] Unable to connect to the database...try again in a while.') + import time + time.sleep(10) + check_db_connection(retry = retry - 1) + else: + connection.close() + +def check_solr_connection(retry=None): + + print('[prerun] Start check_solr_connection...') + + if retry is None: + retry = RETRY + elif retry == 0: + print('[prerun] Giving up after 5 tries...') + sys.exit(1) + + url = os.environ.get('CKAN_SOLR_URL', '') + username = os.environ.get('SOLR_ADMIN_USERNAME', 'admin') + password = os.environ.get('SOLR_ADMIN_PASSWORD', 'pass') + search_url = '{url}/schema/name?wt=json'.format(url=url) + + try: + if not username: + connection = urllib.request.urlopen(search_url) + else: + passman = urllib.request.HTTPPasswordMgrWithDefaultRealm() + passman.add_password(None, search_url, username, password) + authhandler = urllib.request.HTTPBasicAuthHandler(passman) + opener = urllib.request.build_opener(authhandler) + urllib.request.install_opener(opener) + connection = urllib.request.urlopen(search_url) + except urllib.error.URLError as e: + print('[prerun] Unable to connect to solr...try again in a while.') + import time + time.sleep(10) + check_solr_connection(retry = retry - 1) + else: + import re + conn_info = connection.read() + schema_name = json.loads(conn_info) + if 'ckan' in schema_name['name']: + print('[prerun] Succesfully connected to solr and CKAN schema loaded') + else: + print('[prerun] Succesfully connected to solr, but CKAN schema not found') + sys.exit(1) + +def init_db(): + + print('[prerun] Start init_db...') + + db_command = ['ckan', '-c', ckan_ini, 'db', 'init'] + + print('[prerun] Initializing or upgrading db - start using ckan db init') + try: + # run init scripts + subprocess.check_output(db_command, stderr=subprocess.STDOUT) + + print('[prerun] Initializing or upgrading db - end') + except subprocess.CalledProcessError as e: + if 'OperationalError' in str(e.output): + print(e.output.decode('utf-8')) + print('[prerun] Database not ready, waiting a bit before exit...') + import time + time.sleep(5) + sys.exit(1) + else: + print(e.output.decode('utf-8')) + raise e + print('[prerun] Initializing or upgrading db - finish') + + +def init_datastore(): + + conn_str = os.environ.get('CKAN_DATASTORE_WRITE_URL') + if not conn_str: + print('[prerun] Skipping datastore initialization') + return + + datastore_perms_command = ['ckan', '-c', ckan_ini, 'datastore', + 'set-permissions'] + + db_user = make_url(conn_str).username + db_passwd = make_url(conn_str).password + db_host = make_url(conn_str).host + db_name = make_url(conn_str).database + connection = psycopg2.connect(user=db_user, + host=db_host, + password=db_passwd, + database=db_name) + cursor = connection.cursor() + + print('[prerun] Initializing datastore db - start') + try: + datastore_perms = subprocess.Popen( + datastore_perms_command, + stdout=subprocess.PIPE) + + perms_sql = datastore_perms.stdout.read() + perms_sql = perms_sql.decode('utf-8') + perms_sql = perms_sql.replace("@"+db_host, "") + # Remove internal pg command as psycopg2 does not like it + perms_sql = re.sub('\\\\connect \"(.*)\"', '', perms_sql) + cursor.execute(perms_sql) + for notice in connection.notices: + print(notice) + + connection.commit() + + print('[prerun] Initializing datastore db - end') + print((datastore_perms.stdout.read())) + except psycopg2.Error as e: + print('[prerun] Could not initialize datastore') + print(e.decode('utf-8')) + + except subprocess.CalledProcessError as e: + if 'OperationalError' in str(e.output): + print(e.output.decode('utf-8')) + print('[prerun] Database not ready, waiting a bit before exit...') + time.sleep(5) + sys.exit(1) + else: + print(e.output.decode('utf-8')) + raise e + finally: + cursor.close() + connection.close() + + +def create_sysadmin(): + + print('[prerun] Start create_sysadmin...') + + name = os.environ.get('CKAN_SYSADMIN_NAME') + password = os.environ.get('CKAN_SYSADMIN_PASSWORD') + email = os.environ.get('CKAN_SYSADMIN_EMAIL') + + if name and password and email: + + # Check if user exists + command = ['ckan', '-c', ckan_ini, 'user', 'show', name] + + out = subprocess.check_output(command) + if 'User:None' not in re.sub(r'\s', '', out.decode('utf-8')): + print('[prerun] Sysadmin user exists, skipping creation') + return + + # Create user + command = ['ckan', '-c', ckan_ini, 'user', 'add', + name, + 'password=' + password, + 'email=' + email] + + subprocess.call(command) + print(('[prerun] Created user {0}'.format(name))) + + # Make it sysadmin + command = ['ckan', '-c', ckan_ini, 'sysadmin', 'add', + name] + + subprocess.call(command) + print(('[prerun] Made user {0} a sysadmin'.format(name))) + +if __name__ == '__main__': + + maintenance = os.environ.get('MAINTENANCE_MODE', '').lower() == 'true' + + if maintenance: + print('[prerun] Maintenance mode, skipping setup...') + else: + check_db_connection() + check_solr_connection() + init_db() + if os.environ.get('CKAN_DATASTORE_WRITE_URL'): + init_datastore() + create_sysadmin() diff --git a/images/ckan/2.10/setup/app/start_ckan.sh b/images/ckan/2.10/setup/app/start_ckan.sh new file mode 100755 index 0000000..f1b5986 --- /dev/null +++ b/images/ckan/2.10/setup/app/start_ckan.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Run any startup scripts provided by images extending this one +if [[ -d "${APP_DIR}/docker-entrypoint.d" ]] +then + for f in ${APP_DIR}/docker-entrypoint.d/*; do + case "$f" in + *.sh) echo "$0: Running init file $f"; . "$f" ;; + *.py) echo "$0: Running init file $f"; python "$f"; echo ;; + *) echo "$0: Ignoring $f (not an sh or py file)" ;; + esac + echo + done +fi + +if grep -E "beaker.session.secret ?= ?$" $APP_DIR/production.ini +then + echo "Setting secrets in ini file" + ckan config-tool $APP_DIR/production.ini "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" + ckan config-tool $APP_DIR/production.ini "api_token.jwt.encode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" + ckan config-tool $APP_DIR/production.ini "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" +fi + +echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" +UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid ckan --gid ckan --http :5000 --master --enable-threads --wsgi-file /srv/app/wsgi.py --module wsgi:application --lazy-apps --gevent 2000 -p ${UWSGI_PROC_NO:-2} -L --gevent-early-monkey-patch --vacuum --harakiri 50 --callable application" + +# Run the prerun script to init CKAN and create the default admin user +python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } + +# Check if we are in maintenance mode and if yes serve the maintenance pages +if [ "$MAINTENANCE_MODE" = true ]; then PYTHONUNBUFFERED=1 python maintenance/serve.py; fi + +# Run any after prerun/init scripts provided by images extending this one +if [[ -d "${APP_DIR}/docker-afterinit.d" ]] +then + for f in ${APP_DIR}/docker-afterinit.d/*; do + case "$f" in + *.sh) echo "$0: Running after prerun init file $f"; . "$f" ;; + *.py) echo "$0: Running after prerun init file $f"; python "$f"; echo ;; + *) echo "$0: Ignoring $f (not an sh or py file)" ;; + esac + echo + done +fi + +# Check whether http basic auth password protection is enabled and enable basicauth routing on uwsgi respecfully +if [ $? -eq 0 ] +then + if [ "$PASSWORD_PROTECT" = true ] + then + if [ "$HTPASSWD_USER" ] || [ "$HTPASSWD_PASSWORD" ] + then + # Generate htpasswd file for basicauth + htpasswd -d -b -c /srv/app/.htpasswd $HTPASSWD_USER $HTPASSWD_PASSWORD + # Start uwsgi with basicauth + uwsgi --ini /srv/app/uwsgi.conf --pcre-jit $UWSGI_OPTS + else + echo "Missing HTPASSWD_USER or HTPASSWD_PASSWORD environment variables. Exiting..." + exit 1 + fi + else + # Start uwsgi + uwsgi $UWSGI_OPTS + fi +else + echo "[prerun] failed...not starting CKAN." +fi diff --git a/images/ckan/2.10/setup/app/uwsgi.conf b/images/ckan/2.10/setup/app/uwsgi.conf new file mode 100644 index 0000000..6321d6d --- /dev/null +++ b/images/ckan/2.10/setup/app/uwsgi.conf @@ -0,0 +1,2 @@ +[uwsgi] +route = ^(?!/api).*$ basicauth:Restricted,/srv/app/.htpasswd diff --git a/images/ckan/2.10/setup/app/wsgi.py b/images/ckan/2.10/setup/app/wsgi.py new file mode 100644 index 0000000..63f3a7c --- /dev/null +++ b/images/ckan/2.10/setup/app/wsgi.py @@ -0,0 +1,28 @@ +""" +Copyright (c) 2016 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# -*- coding: utf-8 -*- + +import os +from ckan.config.middleware import make_app +from ckan.cli import CKANConfigLoader +from logging.config import fileConfig as loggingFileConfig +config_filepath = os.path.join( + os.path.dirname(os.path.abspath(__file__)), u'production.ini') +abspath = os.path.join(os.path.dirname(os.path.abspath(__file__))) +loggingFileConfig(config_filepath) +config = CKANConfigLoader(config_filepath).get_config() +application = make_app(config) From 07af9e29a086369792e94aed80273e9e72f068a8 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Wed, 22 Feb 2023 11:20:12 +0100 Subject: [PATCH 141/213] created 2.10 compose --- compose/2.10/.ckan-env | 42 ++++++++ compose/2.10/.env | 35 +++++++ compose/2.10/docker-compose.yml | 96 +++++++++++++++++++ compose/2.10/postgresql/Dockerfile | 13 +++ .../00_create_datastore.sh | 8 ++ .../20_postgis_permissions.sql | 2 + compose/2.10/solr8/ckan_init_solr.sh | 38 ++++++++ 7 files changed, 234 insertions(+) create mode 100644 compose/2.10/.ckan-env create mode 100644 compose/2.10/.env create mode 100644 compose/2.10/docker-compose.yml create mode 100644 compose/2.10/postgresql/Dockerfile create mode 100644 compose/2.10/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh create mode 100644 compose/2.10/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql create mode 100755 compose/2.10/solr8/ckan_init_solr.sh diff --git a/compose/2.10/.ckan-env b/compose/2.10/.ckan-env new file mode 100644 index 0000000..5ad12d2 --- /dev/null +++ b/compose/2.10/.ckan-env @@ -0,0 +1,42 @@ +# Runtime configuration of CKAN enabled through ckanext-envvars +# Information about how it works: https://github.com/okfn/ckanext-envvars +# Note that variables here take presedence over build/up time variables in .env + +# Set to true to disable CKAN from starting and serve a maintenance page +MAINTENANCE_MODE=false + +# General Settings +CKAN_SITE_ID=default +CKAN_SITE_URL=http://localhost:5005 +CKAN_PORT=5005 +CKAN_MAX_UPLOAD_SIZE_MB=20 +CKAN___BEAKER__SESSION__SECRET=CHANGE_ME +# See https://docs.ckan.org/en/latest/maintaining/configuration.html#api-token-settings +CKAN___API_TOKEN__JWT__ENCODE__SECRET=string:CHANGE_ME +CKAN___API_TOKEN__JWT__DECODE__SECRET=string:CHANGE_ME +# CKAN Plugins +CKAN__PLUGINS=envvars image_view text_view recline_view datastore datapusher +# CKAN requires storage path to be set in order for filestore to be enabled +CKAN__STORAGE_PATH=/srv/app/data +CKAN__WEBASSETS__PATH=/srv/app/data/webassets +# SYSADMIN settings, a sysadmin user is created automatically with the below credentials +CKAN_SYSADMIN_NAME=sysadmin +CKAN_SYSADMIN_PASSWORD=password +CKAN_SYSADMIN_EMAIL=sysadmin@ckantest.com + +# Email settings +CKAN_SMTP_SERVER=smtp.corporateict.domain:25 +CKAN_SMTP_STARTTLS=True +CKAN_SMTP_USER=user +CKAN_SMTP_PASSWORD=pass +CKAN_SMTP_MAIL_FROM=ckan@localhost + +# Datapusher configuration +CKAN__DATAPUSHER__URL=http://datapusher:8000 +CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000/ +CKAN__DATAPUSHER__API_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ6M0lGRGw4SVdFR3lVRzFYRHJ0cm56WmNmcDlzbmFuUHlUMjdqLXRVSW9nIiwiaWF0IjoxNjc3MDYwNjQ1fQ.ECCa0wU-FOOGpdE9_TTVj__2J9SfANRBD6gRClBvPk0 +# CKAN__DATAPUSHER__API_TOKEN=replace_this_with_api_token_once_ckan_starts + +# Solr configuration +CKAN_VERSION=2.9.7 +CKAN_CORE_NAME=ckan \ No newline at end of file diff --git a/compose/2.10/.env b/compose/2.10/.env new file mode 100644 index 0000000..3a722e6 --- /dev/null +++ b/compose/2.10/.env @@ -0,0 +1,35 @@ +# Variables in this file will be used as build arguments when running +# docker-compose build and docker-compose up +# Verify correct substitution with "docker-compose config" +# If variables are newly added or enabled, please delete and rebuild the images to pull in changes: +# docker-compose down -v +# docker-compose build +# docker-compose up -d + +# Database +POSTGRES_PASSWORD=ckan +POSTGRES_PORT=5432 +DATASTORE_READONLY_PASSWORD=datastore + +# CKAN +CKAN_VERSION=2.9.7 +CKAN_SITE_ID=default +CKAN_SITE_URL=http://localhost:5005 +CKAN_PORT=5005 +CKAN_MAX_UPLOAD_SIZE_MB=10 + +# Datapusher +DATAPUSHER_VERSION=0.0.17 +DATAPUSHER_MAX_CONTENT_LENGTH=10485760 +DATAPUSHER_CHUNK_SIZE=16384 +DATAPUSHER_CHUNK_INSERT_ROWS=250 +DATAPUSHER_DOWNLOAD_TIMEOUT=30 +DATAPUSHER_SSL_VERIFY=False +DATAPUSHER_REWRITE_RESOURCES=True +DATAPUSHER_REWRITE_URL=http://ckan:5005 + +# SOLR +CKAN_CORE_NAME=ckan + +# Redis +REDIS_VERSION=6.0.7 diff --git a/compose/2.10/docker-compose.yml b/compose/2.10/docker-compose.yml new file mode 100644 index 0000000..77c9126 --- /dev/null +++ b/compose/2.10/docker-compose.yml @@ -0,0 +1,96 @@ +# docker-compose build && docker-compose up -d +version: "3" + +volumes: + ckan_data: + pg_data: + solr_data: + +services: + ckan: + container_name: ckan + build: /home/filip/Desktop/Projects/localdev/docker-ckan/images/ckan/2.10/ + networks: + - frontend + - backend + depends_on: + - db + - solr + ports: + - "0.0.0.0:${CKAN_PORT}:5000" + env_file: + - ./.ckan-env + environment: + - CKAN_SQLALCHEMY_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/ckan + - CKAN_DATASTORE_WRITE_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/datastore + - CKAN_DATASTORE_READ_URL=postgresql://datastore_ro:${DATASTORE_READONLY_PASSWORD}@db/datastore + - CKAN_SOLR_URL=http://solr:8983/solr/ckan + - CKAN_REDIS_URL=redis://redis:6379/1 + - CKAN_SITE_URL=${CKAN_SITE_URL} + - CKAN_MAX_UPLOAD_SIZE_MB=${CKAN_MAX_UPLOAD_SIZE_MB} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + + volumes: + - ckan_data:/srv/app/data + + datapusher: + container_name: datapusher + image: ghcr.io/keitaroinc/datapusher:${DATAPUSHER_VERSION} + networks: + - frontend + - backend + ports: + - "8000:8000" + environment: + - DATAPUSHER_MAX_CONTENT_LENGTH=${DATAPUSHER_MAX_CONTENT_LENGTH} + - DATAPUSHER_CHUNK_SIZE=${DATAPUSHER_CHUNK_SIZE} + - DATAPUSHER_CHUNK_INSERT_ROWS=${DATAPUSHER_CHUNK_INSERT_ROWS} + - DATAPUSHER_DOWNLOAD_TIMEOUT=${DATAPUSHER_DOWNLOAD_TIMEOUT} + - DATAPUSHER_SSL_VERIFY=${DATA_PUSHER_SSL_VERIFY} + - DATAPUSHER_REWRITE_RESOURCES=${DATAPUSHER_REWRITE_RESOURCES} + - DATAPUSHER_REWRITE_URL=${DATAPUSHER_REWRITE_URL} + + + db: + container_name: db + build: + context: . + dockerfile: postgresql/Dockerfile + args: + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + networks: + - backend + environment: + - DS_RO_PASS=${DATASTORE_READONLY_PASSWORD} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - PGDATA=/var/lib/postgresql/data/db + volumes: + - pg_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "ckan"] + + solr: + container_name: solr + image: solr:8.11.1 + networks: + - backend + env_file: + - ./.ckan-env + environment: + - CKAN_CORE_NAME=${CKAN_CORE_NAME} + - CKAN_VERSION=${CKAN_VERSION} + volumes: + - solr_data:/var/solr + - ${PWD}/solr8/ckan_init_solr.sh:/docker-entrypoint-initdb.d/ckan_init_solr.sh + + redis: + container_name: redis + image: redis:${REDIS_VERSION} + networks: + - backend + +networks: + frontend: + backend: diff --git a/compose/2.10/postgresql/Dockerfile b/compose/2.10/postgresql/Dockerfile new file mode 100644 index 0000000..e5fcbb7 --- /dev/null +++ b/compose/2.10/postgresql/Dockerfile @@ -0,0 +1,13 @@ +FROM postgis/postgis:14-3.2-alpine + +# Allow connections; we don't map out any ports so only linked docker containers can connect +RUN echo "host all all 0.0.0.0/0 md5" >> /var/lib/postgresql/data/pg_hba.conf + +# Customize default user/pass/db +ENV POSTGRES_DB ckan +ENV POSTGRES_USER ckan +ARG POSTGRES_PASSWORD +ARG DS_RO_PASS + +# Include datastore setup scripts +COPY ./postgresql/docker-entrypoint-initdb.d /docker-entrypoint-initdb.d diff --git a/compose/2.10/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh b/compose/2.10/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh new file mode 100644 index 0000000..ec1b3c3 --- /dev/null +++ b/compose/2.10/postgresql/docker-entrypoint-initdb.d/00_create_datastore.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + CREATE ROLE datastore_ro NOSUPERUSER NOCREATEDB NOCREATEROLE LOGIN PASSWORD '$DS_RO_PASS'; + CREATE DATABASE datastore OWNER ckan ENCODING 'utf-8'; + GRANT ALL PRIVILEGES ON DATABASE datastore TO ckan; +EOSQL \ No newline at end of file diff --git a/compose/2.10/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql b/compose/2.10/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql new file mode 100644 index 0000000..8b5348e --- /dev/null +++ b/compose/2.10/postgresql/docker-entrypoint-initdb.d/20_postgis_permissions.sql @@ -0,0 +1,2 @@ +ALTER VIEW geometry_columns OWNER TO ckan; +ALTER TABLE spatial_ref_sys OWNER TO ckan; diff --git a/compose/2.10/solr8/ckan_init_solr.sh b/compose/2.10/solr8/ckan_init_solr.sh new file mode 100755 index 0000000..8ea06c3 --- /dev/null +++ b/compose/2.10/solr8/ckan_init_solr.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# +# Initialize SOLR for CKAN by creating a ckan core +# Arguments are supplied via environment variables: CKAN_CORE_NAME CKAN_VERSION +# Example: +# CKAN_CORE_NAME=ckan +# CKAN_VERSION=2.9.7 + +set -e + +CKAN_SOLR_SCHEMA_URL=https://raw.githubusercontent.com/ckan/ckan/ckan-$CKAN_VERSION/ckan/config/solr/schema.solr8.xml + +echo "Check whether managed schema exists for CKAN $CKAN_VERSION" +if ! curl --output /dev/null --silent --head --fail "$CKAN_SOLR_SCHEMA_URL"; then + echo "Can't find CKAN SOLR schema at URL: $CKAN_SOLR_SCHEMA_URL. Exiting..." + exit 1 +fi + +echo "Check whether SOLR is initialized for CKAN" +CORESDIR=/var/solr/data + +COREDIR="$CORESDIR/$CKAN_CORE_NAME" +if [ -d "$COREDIR" ]; then + echo "SOLR already initialized, skipping initialization" +else + echo "Initializing SOLR core $CKAN_CORE_NAME for CKAN $CKAN_VERSION" + # init script for handling an empty /var/solr + /opt/docker-solr/scripts/init-var-solr + + # Precreate CKAN core + /opt/docker-solr/scripts/precreate-core $CKAN_CORE_NAME + + # Replace the managed schema with CKANs schema + echo "Adding CKAN managed schema" + curl $CKAN_SOLR_SCHEMA_URL -o /var/solr/data/$CKAN_CORE_NAME/conf/managed-schema -s + + echo "SOLR initialized" +fi From 24b95e7cad8a025e7ea7c7ca47168d085df9fc63 Mon Sep 17 00:00:00 2001 From: Kiril Poposki Date: Wed, 22 Feb 2023 11:28:33 +0100 Subject: [PATCH 142/213] fixing docker-compose build for ckan 2.10 --- compose/2.10/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/2.10/docker-compose.yml b/compose/2.10/docker-compose.yml index 77c9126..bcd3f60 100644 --- a/compose/2.10/docker-compose.yml +++ b/compose/2.10/docker-compose.yml @@ -9,7 +9,7 @@ volumes: services: ckan: container_name: ckan - build: /home/filip/Desktop/Projects/localdev/docker-ckan/images/ckan/2.10/ + build: ../../images/ckan/2.10/ networks: - frontend - backend From 6942b4650f6690886847133e13c3289ed0fa750f Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Wed, 15 Mar 2023 13:14:32 +0100 Subject: [PATCH 143/213] updated ckan 2.9.7 to 2.9.8 --- Readme.md | 8 ++++---- compose/2.9/.ckan-env | 2 +- compose/2.9/.env | 2 +- compose/2.9/solr8/ckan_init_solr.sh | 2 +- examples/harvest/.ckan-env | 2 +- examples/harvest/.env | 2 +- examples/harvest/Dockerfile | 4 ++-- examples/s3filestore/.ckan-env | 2 +- examples/s3filestore/.env | 2 +- examples/s3filestore/Dockerfile | 4 ++-- images/ckan/2.9/Dockerfile | 4 ++-- images/ckan/2.9/Dockerfile.focal | 4 ++-- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Readme.md b/Readme.md index b3da9e0..b5bfecf 100644 --- a/Readme.md +++ b/Readme.md @@ -48,7 +48,7 @@ We recommend to use a multi-stage approach to extend the docker images that we p ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.7 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.8 as extbuild # Switch to the root user USER root @@ -64,7 +64,7 @@ RUN pip wheel --wheel-dir=/wheels git+https://github.com/acmecorp/ckanext-acme@0 ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.7 +FROM ghcr.io/keitaroinc/ckan:2.9.8 # Add the custom extensions to the plugins list ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher acme @@ -94,9 +94,9 @@ You can add scripts to CKAN custom images and copy them to the *docker-afterinit ## Build To build a CKAN image run: ```sh -docker build --tag ghcr.io/keitaroinc/ckan:2.9.7 images/ckan/2.9 +docker build --tag ghcr.io/keitaroinc/ckan:2.9.8 images/ckan/2.9 ``` -The –-tag ghcr.io/keitaroinc/ckan:2.9.7 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.7 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. +The –-tag ghcr.io/keitaroinc/ckan:2.9.8 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.8 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. ## Upload to DockerHub >*It's recommended to upload built images to DockerHub* diff --git a/compose/2.9/.ckan-env b/compose/2.9/.ckan-env index 9f7e329..250b161 100644 --- a/compose/2.9/.ckan-env +++ b/compose/2.9/.ckan-env @@ -36,5 +36,5 @@ CKAN__DATAPUSHER__URL=http://datapusher:8000 CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000/ # Solr configuration -CKAN_VERSION=2.9.7 +CKAN_VERSION=2.9.8 CKAN_CORE_NAME=ckan diff --git a/compose/2.9/.env b/compose/2.9/.env index 0780e0c..1765a6b 100644 --- a/compose/2.9/.env +++ b/compose/2.9/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.7 +CKAN_VERSION=2.9.8 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/2.9/solr8/ckan_init_solr.sh b/compose/2.9/solr8/ckan_init_solr.sh index 8ea06c3..680a1fa 100755 --- a/compose/2.9/solr8/ckan_init_solr.sh +++ b/compose/2.9/solr8/ckan_init_solr.sh @@ -4,7 +4,7 @@ # Arguments are supplied via environment variables: CKAN_CORE_NAME CKAN_VERSION # Example: # CKAN_CORE_NAME=ckan -# CKAN_VERSION=2.9.7 +# CKAN_VERSION=2.9.8 set -e diff --git a/examples/harvest/.ckan-env b/examples/harvest/.ckan-env index d8d32dd..b745b6f 100644 --- a/examples/harvest/.ckan-env +++ b/examples/harvest/.ckan-env @@ -33,5 +33,5 @@ CKAN__HARVEST__MQ__TYPE=redis CKAN__HARVEST__MQ__HOSTNAME=redis # Solr configuration -CKAN_VERSION=2.9.7 +CKAN_VERSION=2.9.8 CKAN_CORE_NAME=ckan diff --git a/examples/harvest/.env b/examples/harvest/.env index 0780e0c..1765a6b 100644 --- a/examples/harvest/.env +++ b/examples/harvest/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.7 +CKAN_VERSION=2.9.8 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index 0071ba3..3a2c008 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.7 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.8 as extbuild # Locations and tags, please use specific tags or revisions ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest @@ -30,7 +30,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.7 +FROM ghcr.io/keitaroinc/ckan:2.9.8 LABEL maintainer="Keitaro Inc " diff --git a/examples/s3filestore/.ckan-env b/examples/s3filestore/.ckan-env index 24c883a..2800308 100644 --- a/examples/s3filestore/.ckan-env +++ b/examples/s3filestore/.ckan-env @@ -38,5 +38,5 @@ CKANEXT__S3FILESTORE__REGION_NAME=us-east-1 CKANEXT__S3FILESTORE__SIGNATURE_VERSION=s3v4 # Solr configuration -CKAN_VERSION=2.9.7 +CKAN_VERSION=2.9.8 CKAN_CORE_NAME=ckan diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env index 0780e0c..1765a6b 100644 --- a/examples/s3filestore/.env +++ b/examples/s3filestore/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.7 +CKAN_VERSION=2.9.8 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index dd030b5..166952d 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.7 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.8 as extbuild # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore @@ -20,7 +20,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.7 +FROM ghcr.io/keitaroinc/ckan:2.9.8 LABEL maintainer="Keitaro Inc " diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index aba8ebb..5458289 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.13.7 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.7 +ENV IMAGE_TAG=2.9.8 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.7 +ENV GIT_BRANCH=ckan-2.9.8 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 369d7fd..3e6abc8 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.7-focal +ENV IMAGE_TAG=2.9.8-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.7 +ENV GIT_BRANCH=ckan-2.9.8 # Set timezone ENV TZ=UTC From dd75dc96c1d5671565f291652730a3fc2360c111 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Wed, 15 Mar 2023 13:16:44 +0100 Subject: [PATCH 144/213] added solr configmap for 2.9.8 --- compose/2.8/solr/solrconfig-2.9.8.xml | 203 ++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 compose/2.8/solr/solrconfig-2.9.8.xml diff --git a/compose/2.8/solr/solrconfig-2.9.8.xml b/compose/2.8/solr/solrconfig-2.9.8.xml new file mode 100644 index 0000000..9b90496 --- /dev/null +++ b/compose/2.8/solr/solrconfig-2.9.8.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +index_id + + + + + + + + + + + + + + + + + + + + + + + + + From 6b7de9a4a334618ad4a039ea2c8cf57d3ef57acb Mon Sep 17 00:00:00 2001 From: Petar Popovski Date: Fri, 17 Mar 2023 16:00:35 +0100 Subject: [PATCH 145/213] Patch for sending mail with aws - ascii encoding problem --- .../2.9/patches/03_patch_smtp_aws_sent_from.patch | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch diff --git a/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch b/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch new file mode 100644 index 0000000..a74b489 --- /dev/null +++ b/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch @@ -0,0 +1,13 @@ +diff --git a/ckan/lib/mailer.py b/ckan/lib/mailer.py +index dc83eb0f1..b9839a70d 100644 +--- a/ckan/lib/mailer.py ++++ b/ckan/lib/mailer.py +@@ -55,7 +55,7 @@ def _mail_recipient(recipient_name, recipient_email, + msg.add_header(k, v) + subject = Header(subject.encode('utf-8'), 'utf-8') + msg['Subject'] = subject +- msg['From'] = _("%s <%s>") % (sender_name, mail_from) ++ msg['From'] = _("%s <%s>") % ('CKAN', mail_from) + msg['To'] = u"%s <%s>" % (recipient_name, recipient_email) + msg['Date'] = utils.formatdate(time()) + msg['X-Mailer'] = "CKAN %s" % ckan.__version__ From e64819da932c7a7ff27ba72088d3b726b5bf70f8 Mon Sep 17 00:00:00 2001 From: blagoja Date: Mon, 20 Mar 2023 10:34:04 +0100 Subject: [PATCH 146/213] Path fix on mailer.py --- images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch b/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch index a74b489..7dff4bc 100644 --- a/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch +++ b/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch @@ -1,7 +1,5 @@ -diff --git a/ckan/lib/mailer.py b/ckan/lib/mailer.py -index dc83eb0f1..b9839a70d 100644 ---- a/ckan/lib/mailer.py -+++ b/ckan/lib/mailer.py +--- ckan/ckan/lib/mailer.py ++++ ckan/ckan/lib/mailer.py @@ -55,7 +55,7 @@ def _mail_recipient(recipient_name, recipient_email, msg.add_header(k, v) subject = Header(subject.encode('utf-8'), 'utf-8') From 040336f8545b593d06bcb448ed6befc35cbb5516 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Mon, 10 Apr 2023 11:14:03 +0200 Subject: [PATCH 147/213] changed name of solr vars --- images/ckan/2.9/setup/app/prerun.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/images/ckan/2.9/setup/app/prerun.py b/images/ckan/2.9/setup/app/prerun.py index 626c42b..cd95b16 100644 --- a/images/ckan/2.9/setup/app/prerun.py +++ b/images/ckan/2.9/setup/app/prerun.py @@ -68,8 +68,8 @@ def check_solr_connection(retry=None): sys.exit(1) url = os.environ.get('CKAN_SOLR_URL', '') - username = os.environ.get('SOLR_ADMIN_USERNAME', '') - password = os.environ.get('SOLR_ADMIN_PASSWORD', '') + username = os.environ.get('CKAN_SOLR_USER', '') + password = os.environ.get('CKAN_SOLR_PASSWORD', '') search_url = '{url}/schema/name?wt=json'.format(url=url) From 9aa3042d986a11367590af802d19ace0fe2445cb Mon Sep 17 00:00:00 2001 From: blagoja Date: Tue, 25 Apr 2023 15:41:45 +0200 Subject: [PATCH 148/213] Recipent name patch for names containing no utf-8 chars --- .../patches/04_patch_smtp_aws_recipient_name.patch | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 images/ckan/2.9/patches/04_patch_smtp_aws_recipient_name.patch diff --git a/images/ckan/2.9/patches/04_patch_smtp_aws_recipient_name.patch b/images/ckan/2.9/patches/04_patch_smtp_aws_recipient_name.patch new file mode 100644 index 0000000..7d51bf1 --- /dev/null +++ b/images/ckan/2.9/patches/04_patch_smtp_aws_recipient_name.patch @@ -0,0 +1,11 @@ +--- a/ckan/lib/mailer.py ++++ b/ckan/lib/mailer.py +@@ -121,7 +121,7 @@ def mail_recipient(recipient_name, recipient_email, subject, + '''Sends an email''' + site_title = config.get('ckan.site_title') + site_url = config.get('ckan.site_url') +- return _mail_recipient(recipient_name, recipient_email, ++ return _mail_recipient("", recipient_email, + site_title, site_url, subject, body, + body_html=body_html, headers=headers) + From 9fa00a9a2471b11c2748aaa20cec24bbc45424c7 Mon Sep 17 00:00:00 2001 From: Blagoja Bozinovski <62408051+blagojabozinovski@users.noreply.github.com> Date: Thu, 27 Apr 2023 08:39:57 +0200 Subject: [PATCH 149/213] Delete 04_patch_smtp_aws_recipient_name.patch Error when solving the patches when building the image. --- .../patches/04_patch_smtp_aws_recipient_name.patch | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 images/ckan/2.9/patches/04_patch_smtp_aws_recipient_name.patch diff --git a/images/ckan/2.9/patches/04_patch_smtp_aws_recipient_name.patch b/images/ckan/2.9/patches/04_patch_smtp_aws_recipient_name.patch deleted file mode 100644 index 7d51bf1..0000000 --- a/images/ckan/2.9/patches/04_patch_smtp_aws_recipient_name.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- a/ckan/lib/mailer.py -+++ b/ckan/lib/mailer.py -@@ -121,7 +121,7 @@ def mail_recipient(recipient_name, recipient_email, subject, - '''Sends an email''' - site_title = config.get('ckan.site_title') - site_url = config.get('ckan.site_url') -- return _mail_recipient(recipient_name, recipient_email, -+ return _mail_recipient("", recipient_email, - site_title, site_url, subject, body, - body_html=body_html, headers=headers) - From 5c9c5920b4a149f54095d777a8b6eb7cc38cf735 Mon Sep 17 00:00:00 2001 From: blagoja Date: Tue, 2 May 2023 11:36:26 +0200 Subject: [PATCH 150/213] Email sender recipient fix --- .../2.9/patches/03_patch_smtp_aws_sent_from.patch | 2 +- .../2.9/patches/04_patch_smtp_aws_recipient.patch | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 images/ckan/2.9/patches/04_patch_smtp_aws_recipient.patch diff --git a/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch b/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch index 7dff4bc..8925798 100644 --- a/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch +++ b/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch @@ -5,7 +5,7 @@ subject = Header(subject.encode('utf-8'), 'utf-8') msg['Subject'] = subject - msg['From'] = _("%s <%s>") % (sender_name, mail_from) -+ msg['From'] = _("%s <%s>") % ('CKAN', mail_from) ++ msg['From'] = utils.formataddr((sender_name, mail_from)) msg['To'] = u"%s <%s>" % (recipient_name, recipient_email) msg['Date'] = utils.formatdate(time()) msg['X-Mailer'] = "CKAN %s" % ckan.__version__ diff --git a/images/ckan/2.9/patches/04_patch_smtp_aws_recipient.patch b/images/ckan/2.9/patches/04_patch_smtp_aws_recipient.patch new file mode 100644 index 0000000..9dbf653 --- /dev/null +++ b/images/ckan/2.9/patches/04_patch_smtp_aws_recipient.patch @@ -0,0 +1,11 @@ +--- ckan/ckan/lib/mailer.py ++++ ckan/ckan/lib/mailer.py +@@ -56,7 +56,7 @@ def _mail_recipient(recipient_name, recipient_email, + subject = Header(subject.encode('utf-8'), 'utf-8') + msg['Subject'] = subject + msg['From'] = utils.formataddr((sender_name, mail_from)) +- msg['To'] = u"%s <%s>" % (recipient_name, recipient_email) ++ msg['To'] = utils.formataddr((recipient_name, recipient_email)) + msg['Date'] = utils.formatdate(time()) + msg['X-Mailer'] = "CKAN %s" % ckan.__version__ + # Check if extension is setting reply-to via headers or use config option From 1a34f6ac4f8d92d9ff7feb7f92f28779950c7648 Mon Sep 17 00:00:00 2001 From: b-a0 <25707742+b-a0@users.noreply.github.com> Date: Tue, 13 Jun 2023 13:13:40 +0200 Subject: [PATCH 151/213] Change git CKAN git branch to v2.9.9 --- images/ckan/2.9/Dockerfile | 74 +++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 5458289..8d075c8 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.13.7 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.8 +ENV IMAGE_TAG=2.9.9 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.8 +ENV GIT_BRANCH=ckan-2.9.9 # Set src dirs ENV SRC_DIR=/srv/app/src @@ -18,25 +18,25 @@ WORKDIR ${SRC_DIR} # Packages to build CKAN requirements and plugins RUN apk add --no-cache \ - python3 \ - python3-dev \ - git \ - curl \ - postgresql-dev \ - linux-headers \ - gcc \ - make \ - g++ \ - autoconf \ - automake \ - libtool \ - patch \ - musl-dev \ - pcre-dev \ - pcre \ - libffi-dev \ - libxml2-dev \ - libxslt-dev + python3 \ + python3-dev \ + git \ + curl \ + postgresql-dev \ + linux-headers \ + gcc \ + make \ + g++ \ + autoconf \ + automake \ + libtool \ + patch \ + musl-dev \ + pcre-dev \ + pcre \ + libffi-dev \ + libxml2-dev \ + libxslt-dev # Link python to python3 RUN ln -s /usr/bin/python3 /usr/bin/python @@ -80,10 +80,10 @@ ENV ENVVARS_GIT_URL=https://github.com/okfn/ckanext-envvars ENV ENVVARS_GIT_BRANCH=0.0.1 RUN apk add --no-cache \ - python3 \ - python3-dev \ - git \ - curl + python3 \ + python3-dev \ + git \ + curl # Link python to python3 RUN ln -s /usr/bin/python3 /usr/bin/python @@ -116,18 +116,18 @@ ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher # Install necessary packages to run CKAN RUN apk add --no-cache \ - python3 \ - bash \ - git \ - gettext \ - curl \ - postgresql-client \ - libmagic \ - pcre \ - libxslt \ - libxml2 \ - tzdata \ - apache2-utils && \ + python3 \ + bash \ + git \ + gettext \ + curl \ + postgresql-client \ + libmagic \ + pcre \ + libxslt \ + libxml2 \ + tzdata \ + apache2-utils && \ # Create SRC_DIR mkdir -p ${SRC_DIR} && \ # Link python to python3 From eef819e0b7308b8aea446330fd297b364d7f3b09 Mon Sep 17 00:00:00 2001 From: b-a0 <25707742+b-a0@users.noreply.github.com> Date: Tue, 13 Jun 2023 13:19:27 +0200 Subject: [PATCH 152/213] Update version in -focal image as well --- images/ckan/2.9/Dockerfile.focal | 84 ++++++++++++++++---------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 6d95041..53adcc0 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.8-focal +ENV IMAGE_TAG=2.9.9-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.8 +ENV GIT_BRANCH=ckan-2.9.9 # Set timezone ENV TZ=UTC @@ -32,29 +32,29 @@ RUN update-locale LANG=${LC_ALL} # Instal apt-utils RUN apt-get install --no-install-recommends -y \ - apt-utils + apt-utils # Packages to build CKAN requirements and plugins RUN apt-get install --no-install-recommends -y \ - git \ - curl \ - ca-certificates \ - python3 \ - libpq-dev \ - linux-headers-generic \ - gcc-10 \ - make \ - g++-10 \ - autoconf \ - automake \ - libtool \ - patch \ - libpcre3-dev \ - libpcre3 \ - python3-dev \ - libffi-dev \ - libxml2-dev \ - libxslt-dev + git \ + curl \ + ca-certificates \ + python3 \ + libpq-dev \ + linux-headers-generic \ + gcc-10 \ + make \ + g++-10 \ + autoconf \ + automake \ + libtool \ + patch \ + libpcre3-dev \ + libpcre3 \ + python3-dev \ + libffi-dev \ + libxml2-dev \ + libxslt-dev # Use gcc 10 RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10 --slave /usr/bin/x86_64-linux-gnu-gcc x86_64-linux-gnu-gcc /usr/bin/x86_64-linux-gnu-gcc-10 @@ -102,11 +102,11 @@ ENV ENVVARS_GIT_BRANCH=0.0.1 RUN apt-get update && \ apt-get install --no-install-recommends -y \ - git \ - curl \ - ca-certificates \ - python3 \ - python3-dev + git \ + curl \ + ca-certificates \ + python3 \ + python3-dev # Link python to python3 RUN ln -s /usr/bin/python3 /usr/bin/python @@ -158,21 +158,21 @@ ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher # Install necessary packages to run CKAN RUN apt-get update && \ apt-get install --no-install-recommends -y \ - gettext \ - curl \ - ca-certificates \ - libpq5 \ - git \ - postgresql-client \ - python3 \ - python3-distutils \ - libpython3.8 \ - libmagic1 \ - libpcre3 \ - libxslt1.1 \ - libxml2 \ - tzdata \ - apache2-utils && \ + gettext \ + curl \ + ca-certificates \ + libpq5 \ + git \ + postgresql-client \ + python3 \ + python3-distutils \ + libpython3.8 \ + libmagic1 \ + libpcre3 \ + libxslt1.1 \ + libxml2 \ + tzdata \ + apache2-utils && \ rm -rf /var/lib/apt/lists/* && \ # Create SRC_DIR mkdir -p ${SRC_DIR} && \ From 7557faec86c2385457b98badb4b5d972232e82aa Mon Sep 17 00:00:00 2001 From: b-a0 <25707742+b-a0@users.noreply.github.com> Date: Tue, 13 Jun 2023 13:20:01 +0200 Subject: [PATCH 153/213] Change version reference in examples and .env files. --- compose/2.9/.ckan-env | 2 +- compose/2.9/.env | 2 +- compose/2.9/solr8/ckan_init_solr.sh | 2 +- examples/harvest/.ckan-env | 2 +- examples/harvest/.env | 2 +- examples/harvest/Dockerfile | 18 +++++++++--------- examples/s3filestore/.ckan-env | 2 +- examples/s3filestore/.env | 2 +- examples/s3filestore/Dockerfile | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/compose/2.9/.ckan-env b/compose/2.9/.ckan-env index 250b161..98a0198 100644 --- a/compose/2.9/.ckan-env +++ b/compose/2.9/.ckan-env @@ -36,5 +36,5 @@ CKAN__DATAPUSHER__URL=http://datapusher:8000 CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000/ # Solr configuration -CKAN_VERSION=2.9.8 +CKAN_VERSION=2.9.9 CKAN_CORE_NAME=ckan diff --git a/compose/2.9/.env b/compose/2.9/.env index 1765a6b..d07bbb5 100644 --- a/compose/2.9/.env +++ b/compose/2.9/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.8 +CKAN_VERSION=2.9.9 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/2.9/solr8/ckan_init_solr.sh b/compose/2.9/solr8/ckan_init_solr.sh index 680a1fa..1e1339e 100755 --- a/compose/2.9/solr8/ckan_init_solr.sh +++ b/compose/2.9/solr8/ckan_init_solr.sh @@ -4,7 +4,7 @@ # Arguments are supplied via environment variables: CKAN_CORE_NAME CKAN_VERSION # Example: # CKAN_CORE_NAME=ckan -# CKAN_VERSION=2.9.8 +# CKAN_VERSION=2.9.9 set -e diff --git a/examples/harvest/.ckan-env b/examples/harvest/.ckan-env index b745b6f..e8ae538 100644 --- a/examples/harvest/.ckan-env +++ b/examples/harvest/.ckan-env @@ -33,5 +33,5 @@ CKAN__HARVEST__MQ__TYPE=redis CKAN__HARVEST__MQ__HOSTNAME=redis # Solr configuration -CKAN_VERSION=2.9.8 +CKAN_VERSION=2.9.9 CKAN_CORE_NAME=ckan diff --git a/examples/harvest/.env b/examples/harvest/.env index 1765a6b..d07bbb5 100644 --- a/examples/harvest/.env +++ b/examples/harvest/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.8 +CKAN_VERSION=2.9.9 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index 3a2c008..2ff4b5d 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.8 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.9 as extbuild # Locations and tags, please use specific tags or revisions ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest @@ -12,13 +12,13 @@ USER root # Install necessary packages to build extensions RUN apk add --no-cache \ - python3-dev \ - gcc \ - g++ \ - libffi-dev \ - openssl-dev \ - rust \ - cargo + python3-dev \ + gcc \ + g++ \ + libffi-dev \ + openssl-dev \ + rust \ + cargo # Fetch and build the custom CKAN extensions RUN pip wheel --wheel-dir=/wheels git+${HARVEST_GIT_URL}@${HARVEST_GIT_BRANCH}#egg=ckanext-harvest @@ -30,7 +30,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.8 +FROM ghcr.io/keitaroinc/ckan:2.9.9 LABEL maintainer="Keitaro Inc " diff --git a/examples/s3filestore/.ckan-env b/examples/s3filestore/.ckan-env index 2800308..90148d1 100644 --- a/examples/s3filestore/.ckan-env +++ b/examples/s3filestore/.ckan-env @@ -38,5 +38,5 @@ CKANEXT__S3FILESTORE__REGION_NAME=us-east-1 CKANEXT__S3FILESTORE__SIGNATURE_VERSION=s3v4 # Solr configuration -CKAN_VERSION=2.9.8 +CKAN_VERSION=2.9.9 CKAN_CORE_NAME=ckan diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env index 1765a6b..d07bbb5 100644 --- a/examples/s3filestore/.env +++ b/examples/s3filestore/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.8 +CKAN_VERSION=2.9.9 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index 166952d..f6487dd 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.8 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.9 as extbuild # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore @@ -20,7 +20,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.8 +FROM ghcr.io/keitaroinc/ckan:2.9.9 LABEL maintainer="Keitaro Inc " From a3ef5702fb445aa12b7a32fba78edb48d0772941 Mon Sep 17 00:00:00 2001 From: b-a0 <25707742+b-a0@users.noreply.github.com> Date: Tue, 13 Jun 2023 13:23:53 +0200 Subject: [PATCH 154/213] Remove e-mail from/to patches as those are integration in base CKAN 2.9.9 --- .../2.9/patches/03_patch_smtp_aws_sent_from.patch | 11 ----------- .../2.9/patches/04_patch_smtp_aws_recipient.patch | 11 ----------- 2 files changed, 22 deletions(-) delete mode 100644 images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch delete mode 100644 images/ckan/2.9/patches/04_patch_smtp_aws_recipient.patch diff --git a/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch b/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch deleted file mode 100644 index 8925798..0000000 --- a/images/ckan/2.9/patches/03_patch_smtp_aws_sent_from.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- ckan/ckan/lib/mailer.py -+++ ckan/ckan/lib/mailer.py -@@ -55,7 +55,7 @@ def _mail_recipient(recipient_name, recipient_email, - msg.add_header(k, v) - subject = Header(subject.encode('utf-8'), 'utf-8') - msg['Subject'] = subject -- msg['From'] = _("%s <%s>") % (sender_name, mail_from) -+ msg['From'] = utils.formataddr((sender_name, mail_from)) - msg['To'] = u"%s <%s>" % (recipient_name, recipient_email) - msg['Date'] = utils.formatdate(time()) - msg['X-Mailer'] = "CKAN %s" % ckan.__version__ diff --git a/images/ckan/2.9/patches/04_patch_smtp_aws_recipient.patch b/images/ckan/2.9/patches/04_patch_smtp_aws_recipient.patch deleted file mode 100644 index 9dbf653..0000000 --- a/images/ckan/2.9/patches/04_patch_smtp_aws_recipient.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- ckan/ckan/lib/mailer.py -+++ ckan/ckan/lib/mailer.py -@@ -56,7 +56,7 @@ def _mail_recipient(recipient_name, recipient_email, - subject = Header(subject.encode('utf-8'), 'utf-8') - msg['Subject'] = subject - msg['From'] = utils.formataddr((sender_name, mail_from)) -- msg['To'] = u"%s <%s>" % (recipient_name, recipient_email) -+ msg['To'] = utils.formataddr((recipient_name, recipient_email)) - msg['Date'] = utils.formatdate(time()) - msg['X-Mailer'] = "CKAN %s" % ckan.__version__ - # Check if extension is setting reply-to via headers or use config option From 81d758f0136310d1c03523935863e55dfb09e273 Mon Sep 17 00:00:00 2001 From: b-a0 <25707742+b-a0@users.noreply.github.com> Date: Tue, 13 Jun 2023 13:52:55 +0200 Subject: [PATCH 155/213] Update patches based on ckan 2.9.9 --- images/ckan/2.9/patches/00_patch_sql_url.patch | 10 +++++----- ...t.patch => 01_patch_postgres_username_split.patch} | 6 +++--- .../patches/01_patch_resource_replace_upload.patch | 11 ----------- 3 files changed, 8 insertions(+), 19 deletions(-) rename images/ckan/2.9/patches/{02_patch_postgres_username_split.patch => 01_patch_postgres_username_split.patch} (61%) delete mode 100644 images/ckan/2.9/patches/01_patch_resource_replace_upload.patch diff --git a/images/ckan/2.9/patches/00_patch_sql_url.patch b/images/ckan/2.9/patches/00_patch_sql_url.patch index 46a7135..7b4f5ab 100644 --- a/images/ckan/2.9/patches/00_patch_sql_url.patch +++ b/images/ckan/2.9/patches/00_patch_sql_url.patch @@ -1,11 +1,11 @@ ---- ckan/ckan/model/__init__.py 2021-02-16 14:47:06.168327441 +0100 -+++ ckan/ckan/model/__init__.py 2021-02-16 14:48:00.740780218 +0100 -@@ -266,7 +266,7 @@ +--- a/ckan/model/__init__.py ++++ b/ckan/model/__init__.py +@@ -276,7 +276,7 @@ class Repository(): self.reset_alembic_output() alembic_config = AlembicConfig(self._alembic_ini) alembic_config.set_main_option( -- "sqlalchemy.url", str(self.metadata.bind.url) -+ "sqlalchemy.url", str(self.metadata.bind.url).replace('%', '%%') +- "sqlalchemy.url", config.get("sqlalchemy.url") ++ "sqlalchemy.url", config.get("sqlalchemy.url").replace('%', '%%') ) try: sqlalchemy_migrate_version = self.metadata.bind.execute( diff --git a/images/ckan/2.9/patches/02_patch_postgres_username_split.patch b/images/ckan/2.9/patches/01_patch_postgres_username_split.patch similarity index 61% rename from images/ckan/2.9/patches/02_patch_postgres_username_split.patch rename to images/ckan/2.9/patches/01_patch_postgres_username_split.patch index 780b432..ea7cb45 100644 --- a/images/ckan/2.9/patches/02_patch_postgres_username_split.patch +++ b/images/ckan/2.9/patches/01_patch_postgres_username_split.patch @@ -1,6 +1,6 @@ ---- ckan/ckanext/datastore/backend/postgres.py 2021-02-18 11:01:56.692267462 +0100 -+++ ckan/ckanext/datastore/backend/postgres-patch.py 2021-02-18 13:45:16.033193435 +0100 -@@ -1690,7 +1690,7 @@ +--- a/ckanext/datastore/backend/postgres.py ++++ b/ckanext/datastore/backend/postgres.py +@@ -1809,7 +1809,7 @@ class DatastorePostgresqlBackend(DatastoreBackend): read only user. ''' write_connection = self._get_write_engine().connect() diff --git a/images/ckan/2.9/patches/01_patch_resource_replace_upload.patch b/images/ckan/2.9/patches/01_patch_resource_replace_upload.patch deleted file mode 100644 index 3f11fac..0000000 --- a/images/ckan/2.9/patches/01_patch_resource_replace_upload.patch +++ /dev/null @@ -1,11 +0,0 @@ ---- ckan/ckan/logic/action/update.py 2021-02-17 16:46:55.673578728 +0100 -+++ ckan/ckan/logic/action/update-edit.py 2021-02-17 16:47:28.905879170 +0100 -@@ -929,7 +929,7 @@ - - ''' - model = context['model'] -- session = model.Session -+ session = model.meta.create_local_session() - context['session'] = session - - user = context['user'] From 6c54d4713158f58858a5c3dabe462aa7183d1a33 Mon Sep 17 00:00:00 2001 From: b-a0 <25707742+b-a0@users.noreply.github.com> Date: Tue, 13 Jun 2023 13:53:12 +0200 Subject: [PATCH 156/213] Update version references in README. --- Readme.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Readme.md b/Readme.md index b5bfecf..511c630 100644 --- a/Readme.md +++ b/Readme.md @@ -48,7 +48,7 @@ We recommend to use a multi-stage approach to extend the docker images that we p ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.8 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.9 as extbuild # Switch to the root user USER root @@ -64,7 +64,7 @@ RUN pip wheel --wheel-dir=/wheels git+https://github.com/acmecorp/ckanext-acme@0 ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.8 +FROM ghcr.io/keitaroinc/ckan:2.9.9 # Add the custom extensions to the plugins list ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher acme @@ -94,9 +94,9 @@ You can add scripts to CKAN custom images and copy them to the *docker-afterinit ## Build To build a CKAN image run: ```sh -docker build --tag ghcr.io/keitaroinc/ckan:2.9.8 images/ckan/2.9 +docker build --tag ghcr.io/keitaroinc/ckan:2.9.9 images/ckan/2.9 ``` -The –-tag ghcr.io/keitaroinc/ckan:2.9.8 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.8 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. +The –-tag ghcr.io/keitaroinc/ckan:2.9.9 flag sets the image name to ghcr.io/keitaroinc/ckan:2.9.9 and 'images/ckan/2.9' at the end tells docker build to use the context into the specified directory where the Dockerfile and related contents are. ## Upload to DockerHub >*It's recommended to upload built images to DockerHub* From 16cf16dc09d0290bbb7ed1ddd43e7c0736d3223e Mon Sep 17 00:00:00 2001 From: b-a0 <25707742+b-a0@users.noreply.github.com> Date: Tue, 13 Jun 2023 15:44:07 +0200 Subject: [PATCH 157/213] Add proper path in patches --- images/ckan/2.9/patches/00_patch_sql_url.patch | 4 ++-- .../ckan/2.9/patches/01_patch_postgres_username_split.patch | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/images/ckan/2.9/patches/00_patch_sql_url.patch b/images/ckan/2.9/patches/00_patch_sql_url.patch index 7b4f5ab..047607e 100644 --- a/images/ckan/2.9/patches/00_patch_sql_url.patch +++ b/images/ckan/2.9/patches/00_patch_sql_url.patch @@ -1,5 +1,5 @@ ---- a/ckan/model/__init__.py -+++ b/ckan/model/__init__.py +--- ckan/ckan/model/__init__.py ++++ ckan/ckan/model/__init__.py @@ -276,7 +276,7 @@ class Repository(): self.reset_alembic_output() alembic_config = AlembicConfig(self._alembic_ini) diff --git a/images/ckan/2.9/patches/01_patch_postgres_username_split.patch b/images/ckan/2.9/patches/01_patch_postgres_username_split.patch index ea7cb45..6c18355 100644 --- a/images/ckan/2.9/patches/01_patch_postgres_username_split.patch +++ b/images/ckan/2.9/patches/01_patch_postgres_username_split.patch @@ -1,5 +1,5 @@ ---- a/ckanext/datastore/backend/postgres.py -+++ b/ckanext/datastore/backend/postgres.py +--- ckan/ckanext/datastore/backend/postgres.py ++++ ckan/ckanext/datastore/backend/postgres.py @@ -1809,7 +1809,7 @@ class DatastorePostgresqlBackend(DatastoreBackend): read only user. ''' From ad5034d3fe26ecae3cc0be794bbd6c8af0521db0 Mon Sep 17 00:00:00 2001 From: Kiril Poposki Date: Tue, 27 Jun 2023 10:54:40 +0200 Subject: [PATCH 158/213] draw session from environment variables --- images/ckan/2.7/setup/app/start_ckan.sh | 8 ++++++++ images/ckan/2.8/setup/app/start_ckan.sh | 8 ++++++++ images/ckan/2.9/setup/app/start_ckan.sh | 10 ++++++++++ 3 files changed, 26 insertions(+) diff --git a/images/ckan/2.7/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh index a7bf12a..0ddca0d 100755 --- a/images/ckan/2.7/setup/app/start_ckan.sh +++ b/images/ckan/2.7/setup/app/start_ckan.sh @@ -12,6 +12,14 @@ then done fi +# Add session secret from chart +if [[ -z $BEAKER_SESSION_SECRET || -v $BEAKER_SESSION_SECRET ]];then + echo "Not all environment variables are set. Generating sessions..." +else + echo "Setting session secrets from environment variables" + paster --plugin=ckan $APP_DIR/production.ini "beaker.session.secret=$BEAKER_SESSION_SECRET" +fi + if grep -E "beaker.session.secret ?= ?$" $APP_DIR/production.ini then echo "Setting beaker.session.secret in ini file" diff --git a/images/ckan/2.8/setup/app/start_ckan.sh b/images/ckan/2.8/setup/app/start_ckan.sh index a7bf12a..5486656 100755 --- a/images/ckan/2.8/setup/app/start_ckan.sh +++ b/images/ckan/2.8/setup/app/start_ckan.sh @@ -12,6 +12,14 @@ then done fi +# Add session secret from chart +if [[ -z $BEAKER_SESSION_SECRET || -v $BEAKER_SESSION_SECRET ]];then + echo "Not all environment variables are set. Generating sessions..." +else + echo "Setting session secrets from environment variables" + paster --plugin=ckan config-tool $APP_DIR/production.ini "beaker.session.secret=$BEAKER_SESSION_SECRET" +fi + if grep -E "beaker.session.secret ?= ?$" $APP_DIR/production.ini then echo "Setting beaker.session.secret in ini file" diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index f1b5986..9cd7d24 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -12,6 +12,16 @@ then done fi +# Add session secret from chart +if [[ -z $BEAKER_SESSION_SECRET || -v $BEAKER_SESSION_SECRET || -z $JWT_ENCODE_SECRET || -v $JWT_ENCODE_SECRET || -z $JWT_DECODE_SECRET || -v $JWT_DECODE_SECRET ]];then + echo "Not all environment variables are set. Generating sessions..." +else + echo "Setting session secrets from environment variables" + ckan config-tool $APP_DIR/production.ini "beaker.session.secret=$BEAKER_SESSION_SECRET" + ckan config-tool $APP_DIR/production.ini "api_token.jwt.encode.secret=$JWT_ENCODE_SECRET" + ckan config-tool $APP_DIR/production.ini "api_token.jwt.decode.secret=$JWT_DECODE_SECRET" +fi + if grep -E "beaker.session.secret ?= ?$" $APP_DIR/production.ini then echo "Setting secrets in ini file" From db217553ffde0ab41120ceee78441fc92db6d886 Mon Sep 17 00:00:00 2001 From: Kiril Poposki Date: Tue, 27 Jun 2023 11:29:07 +0200 Subject: [PATCH 159/213] add python2 secrets in dockerfile and fixing typos --- images/ckan/2.7/Dockerfile | 3 +++ images/ckan/2.7/setup/app/start_ckan.sh | 2 +- images/ckan/2.8/Dockerfile | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/images/ckan/2.7/Dockerfile b/images/ckan/2.7/Dockerfile index 5974c69..3d90fd7 100644 --- a/images/ckan/2.7/Dockerfile +++ b/images/ckan/2.7/Dockerfile @@ -170,6 +170,9 @@ RUN pip install -e /srv/app/src/ckan && \ # Remove wheels RUN rm -rf /srv/app/wheels /srv/app/ext_wheels +# Install python2 secrets for generating sessions +RUN pip install python2-secrets + # Copy necessary scripts COPY setup/app ${APP_DIR} diff --git a/images/ckan/2.7/setup/app/start_ckan.sh b/images/ckan/2.7/setup/app/start_ckan.sh index 0ddca0d..5486656 100755 --- a/images/ckan/2.7/setup/app/start_ckan.sh +++ b/images/ckan/2.7/setup/app/start_ckan.sh @@ -17,7 +17,7 @@ if [[ -z $BEAKER_SESSION_SECRET || -v $BEAKER_SESSION_SECRET ]];then echo "Not all environment variables are set. Generating sessions..." else echo "Setting session secrets from environment variables" - paster --plugin=ckan $APP_DIR/production.ini "beaker.session.secret=$BEAKER_SESSION_SECRET" + paster --plugin=ckan config-tool $APP_DIR/production.ini "beaker.session.secret=$BEAKER_SESSION_SECRET" fi if grep -E "beaker.session.secret ?= ?$" $APP_DIR/production.ini diff --git a/images/ckan/2.8/Dockerfile b/images/ckan/2.8/Dockerfile index ea24952..3588a75 100644 --- a/images/ckan/2.8/Dockerfile +++ b/images/ckan/2.8/Dockerfile @@ -163,6 +163,9 @@ RUN pip install -e /srv/app/src/ckan && \ # Remove wheels RUN rm -rf /srv/app/wheels /srv/app/ext_wheels +# Install python2 secrets for generating sessions +RUN pip install python2-secrets + # Copy necessary scripts COPY setup/app ${APP_DIR} From 16469a9fa6de7838928d372e20f1732302bb2b50 Mon Sep 17 00:00:00 2001 From: Nadica Rizova Date: Thu, 29 Jun 2023 13:47:18 +0200 Subject: [PATCH 160/213] psql-init dockerfile and scripts added --- images/ckan/2.9/psql-init/Dockerfile | 17 ++ .../ckan/2.9/psql-init/psql-init/psql-init.py | 277 ++++++++++++++++++ .../psql-init/psql-init/set_permissions.sql | 108 +++++++ images/ckan/2.9/psql-init/requirements.txt | 2 + 4 files changed, 404 insertions(+) create mode 100644 images/ckan/2.9/psql-init/Dockerfile create mode 100644 images/ckan/2.9/psql-init/psql-init/psql-init.py create mode 100644 images/ckan/2.9/psql-init/psql-init/set_permissions.sql create mode 100644 images/ckan/2.9/psql-init/requirements.txt diff --git a/images/ckan/2.9/psql-init/Dockerfile b/images/ckan/2.9/psql-init/Dockerfile new file mode 100644 index 0000000..9fa996d --- /dev/null +++ b/images/ckan/2.9/psql-init/Dockerfile @@ -0,0 +1,17 @@ + +# Start with a lightweight base image +FROM python:3.9-alpine + +# Set the working directory in the container +WORKDIR /srv + +# Copy the requirements file to the container +COPY requirements.txt . + +# Install the Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code to the container +COPY psql-init/ . + +CMD ["sleep", "1000"] \ No newline at end of file diff --git a/images/ckan/2.9/psql-init/psql-init/psql-init.py b/images/ckan/2.9/psql-init/psql-init/psql-init.py new file mode 100644 index 0000000..507b551 --- /dev/null +++ b/images/ckan/2.9/psql-init/psql-init/psql-init.py @@ -0,0 +1,277 @@ +""" +Copyright (c) 2020 Keitaro AB + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import os +import sys +import subprocess +import re +import psycopg2 +from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT +from psycopg2.extensions import AsIs +from sqlalchemy.engine.url import make_url + + +ckan_conn_str = os.environ.get('CKAN_SQLALCHEMY_URL', '') +datastorerw_conn_str = os.environ.get('CKAN_DATASTORE_WRITE_URL', '') +datastorero_conn_str = os.environ.get('CKAN_DATASTORE_READ_URL', '') + +master_user = os.environ.get('PSQL_MASTER', '') +master_passwd = os.environ.get('PSQL_PASSWD', '') +master_database = os.environ.get('PSQL_DB', '') + + +class DB_Params: + def __init__(self, conn_str): + self.db_user = make_url(conn_str).username + self.db_passwd = make_url(conn_str).password + self.db_host = make_url(conn_str).host + self.db_name = make_url(conn_str).database + + +def check_db_connection(db_params, retry=None): + + print('Checking whether database is up...') + + if retry is None: + retry = 20 + elif retry == 0: + print('Giving up...') + sys.exit(1) + + try: + con = psycopg2.connect(user=master_user, + host=db_params.db_host, + password=master_passwd, + database=master_database) + + except psycopg2.Error as e: + print((str(e))) + print('Unable to connect to the database...try again in a while.') + import time + time.sleep(30) + check_db_connection(db_params, retry=retry - 1) + else: + con.close() + + +def create_user(db_params): + con = None + try: + con = psycopg2.connect(user=master_user, + host=db_params.db_host, + password=master_passwd, + database=master_database) + con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + cur = con.cursor() + print("Creating user " + db_params.db_user.split("@")[0]) + cur.execute('CREATE ROLE "%s" ' + + 'WITH ' + + 'LOGIN NOSUPERUSER INHERIT ' + + 'CREATEDB NOCREATEROLE NOREPLICATION ' + + 'PASSWORD %s', + (AsIs(db_params.db_user.split("@")[0]), + db_params.db_passwd,)) + except(Exception, psycopg2.DatabaseError) as error: + print("ERROR DB: ", error) + finally: + cur.close() + con.close() + + +def create_db(db_params): + con = None + try: + con = psycopg2.connect(user=master_user, + host=db_params.db_host, + password=master_passwd, + database=master_database) + con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + cur = con.cursor() + cur.execute('GRANT "' + db_params.db_user.split("@") + [0] + '" TO "' + master_user.split("@")[0] + '"') + print("Creating database " + db_params.db_name + " with owner " + + db_params.db_user.split("@")[0]) + cur.execute('CREATE DATABASE ' + db_params.db_name + ' OWNER "' + + db_params.db_user.split("@")[0] + '"') + cur.execute('GRANT ALL PRIVILEGES ON DATABASE ' + + db_params.db_name + ' TO "' + + db_params.db_user.split("@")[0] + '"') + if is_pg_buffercache_enabled(db_params) >= 1: + # FIXME: This is a known issue with pg_buffercache access + # For more info check this thread: + # https://www.postgresql.org/message-id/21009351582737086%40iva6-22e79380f52c.qloud-c.yandex.net + print("Granting privileges on pg_monitor to " + + db_params.db_user.split("@")[0]) + cur.execute('GRANT "pg_monitor" TO "' + db_params.db_user.split("@")[0] + '"') + except(Exception, psycopg2.DatabaseError) as error: + print("ERROR DB: ", error) + finally: + cur.close() + con.close() + + +def is_pg_buffercache_enabled(db_params): + con = None + result = None + try: + con = psycopg2.connect(user=master_user, + host=db_params.db_host, + password=master_passwd, + database=db_params.db_name) + con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + cur = con.cursor() + cur.execute("SELECT count(*) FROM pg_extension " + + "WHERE extname = 'pg_buffercache'") + result = cur.fetchone() + except(Exception, psycopg2.DatabaseError) as error: + print("ERROR DB: ", error) + finally: + cur.close() + con.close() + return result[0] + + +def set_datastore_permissions(datastore_rw_params, datastore_ro_params, sql): + con = None + try: + con = psycopg2.connect(user=master_user, + host=datastore_rw_params.db_host, + password=master_passwd, + database=datastore_rw_params.db_name) + con.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + cur = con.cursor() + cur.execute('GRANT CONNECT ON DATABASE ' + + datastore_rw_params.db_name + + ' TO ' + datastore_ro_params.db_user.split("@")[0]) + if is_pg_buffercache_enabled(datastore_rw_params) >= 1: + print("Granting privileges on pg_monitor to " + + datastore_ro_params.db_user.split("@")[0]) + cur.execute('GRANT ALL PRIVILEGES ON TABLE pg_monitor TO ' + + datastore_ro_params.db_user.split("@")[0]) + print("Setting datastore permissions\n") + print(sql) + cur.execute(sql) + print("Datastore permissions applied.") + except Exception as error: + print("ERROR DB: ", error) + finally: + cur.close() + con.close() + + +if master_user == '' or master_passwd == '' or master_database == '': + print("No master postgresql user provided.") + print("Cannot initialize default CKAN db resources. Exiting!") + sys.exit(1) + +print("Master DB: " + master_database + " Master User: " + master_user) + +ckan_db = DB_Params(ckan_conn_str) +datastorerw_db = DB_Params(datastorerw_conn_str) +datastorero_db = DB_Params(datastorero_conn_str) + + +# Check to see whether we can connect to the database, exit after 10 mins +check_db_connection(ckan_db) + +try: + create_user(ckan_db) +except(Exception, psycopg2.DatabaseError) as error: + print("ERROR DB: ", error) + +try: + create_user(datastorerw_db) +except(Exception, psycopg2.DatabaseError) as error: + print("ERROR DB: ", error) + +try: + create_user(datastorero_db) +except(Exception, psycopg2.DatabaseError) as error: + print("ERROR DB: ", error) + +try: + create_db(ckan_db) +except(Exception, psycopg2.DatabaseError) as error: + print("ERROR DB: ", error) + +try: + create_db(datastorerw_db) +except(Exception, psycopg2.DatabaseError) as error: + print("ERROR DB: ", error) + + +def execute_sql_script(ckan_dbp, datastorero_dbp, datastorerw_dbp, script_path): + # Connect to the database + conn = psycopg2.connect( + user=master_user, + host=datastorerw_dbp.db_host, + password=master_passwd, + database=datastorerw_dbp.db_name + ) + + try: + # Create a cursor + cur = conn.cursor() + + # Execute the SQL script + with open(script_path, 'r') as f: + sql_script = f.read() + + # Replace placeholders with actual values + + sql_script = sql_script.replace('{datastoredb}', datastorerw_dbp.db_name) + sql_script = sql_script.replace('{readuser}', datastorero_dbp.db_user) + sql_script = sql_script.replace('{writeuser}', datastorerw_dbp.db_user) + sql_script = sql_script.replace('{mainuser}', ckan_dbp.db_user) + sql_script = sql_script.replace('{maindb}', ckan_dbp.db_name) + + print("CKAN DB User:", ckan_dbp.db_user) + + # Execute the SQL script + cur.execute(sql_script) + + # Commit the changes + conn.commit() + + print("SQL script executed successfully.") + + # print("CKAN DB User:", ckan_dbp.db_user) + # print("read/write DB User:", datastorerw_dbp.db_user) + # print("read/write DB name:", datastorerw_dbp.db_name) + # print("read/write host:", datastorerw_dbp.db_host) + # print("read DB user:", datastorero_dbp.db_user) + # print("read DB name:", datastorero_dbp.db_name) + + except psycopg2.Error as e: + print(f"Error executing SQL script: {str(e)}") + + finally: + # Close the cursor and the connection + cur.close() + conn.close() + +set_permissions = './set_permissions.sql' + +# Print the current working directory +print("Current working directory:", os.getcwd()) + +# Check if the file exists +if os.path.isfile(set_permissions): + print("File exists.") + # Call the execute_sql_script function with the appropriate arguments + execute_sql_script(ckan_db, datastorero_db, datastorerw_db, set_permissions) +else: + print("File not found.") diff --git a/images/ckan/2.9/psql-init/psql-init/set_permissions.sql b/images/ckan/2.9/psql-init/psql-init/set_permissions.sql new file mode 100644 index 0000000..e7be428 --- /dev/null +++ b/images/ckan/2.9/psql-init/psql-init/set_permissions.sql @@ -0,0 +1,108 @@ +/* +This script configures the permissions for the datastore. + +It ensures that the datastore read-only user will only be able to select from +the datastore database but has no create/write/edit permission or any +permissions on other databases. You must execute this script as a database +superuser on the PostgreSQL server that hosts your datastore database. + +For example, if PostgreSQL is running locally and the "postgres" user has the +appropriate permissions (as in the default Ubuntu PostgreSQL install), you can +run: + + ckan -c /etc/ckan/default/ckan.ini datastore set-permissions | sudo -u postgres psql + +Or, if your PostgreSQL server is remote, you can pipe the permissions script +over SSH: + + ckan -c /etc/ckan/default/ckan.ini datastore set-permissions | ssh dbserver sudo -u postgres psql + +*/ + +-- Most of the following commands apply to an explicit database or to the whole +-- 'public' schema, and could be executed anywhere. But ALTER DEFAULT +-- PERMISSIONS applies to the current database, and so we must be connected to +-- the datastore DB: +--\connect {datastoredb} + +-- revoke permissions for the read-only user +REVOKE CREATE ON SCHEMA public FROM PUBLIC; +REVOKE USAGE ON SCHEMA public FROM PUBLIC; + +GRANT CREATE ON SCHEMA public TO {mainuser}; +GRANT USAGE ON SCHEMA public TO {mainuser}; + +GRANT CREATE ON SCHEMA public TO {writeuser}; +GRANT USAGE ON SCHEMA public TO {writeuser}; + +-- take connect permissions from main db +REVOKE CONNECT ON DATABASE {maindb} FROM {readuser}; + +-- grant select permissions for read-only user +GRANT CONNECT ON DATABASE {datastoredb} TO {readuser}; +GRANT USAGE ON SCHEMA public TO {readuser}; + +-- grant access to current tables and views to read-only user +GRANT SELECT ON ALL TABLES IN SCHEMA public TO {readuser}; + +-- grant access to new tables and views by default +ALTER DEFAULT PRIVILEGES FOR USER {writeuser} IN SCHEMA public + GRANT SELECT ON TABLES TO {readuser}; + +-- a view for listing valid table (resource id) and view names +CREATE OR REPLACE VIEW "_table_metadata" AS + SELECT DISTINCT + substr(md5(dependee.relname || COALESCE(dependent.relname, '')), 0, 17) AS "_id", + dependee.relname AS name, + dependee.oid AS oid, + dependent.relname AS alias_of + FROM + pg_class AS dependee + LEFT OUTER JOIN pg_rewrite AS r ON r.ev_class = dependee.oid + LEFT OUTER JOIN pg_depend AS d ON d.objid = r.oid + LEFT OUTER JOIN pg_class AS dependent ON d.refobjid = dependent.oid + WHERE + (dependee.oid != dependent.oid OR dependent.oid IS NULL) AND + -- is a table (from pg_tables view definition) + -- or is a view (from pg_views view definition) + (dependee.relkind = 'r'::"char" OR dependee.relkind = 'v'::"char") + AND dependee.relnamespace = ( + SELECT oid FROM pg_namespace WHERE nspname='public') + ORDER BY dependee.oid DESC; +ALTER VIEW "_table_metadata" OWNER TO {writeuser}; +GRANT SELECT ON "_table_metadata" TO {readuser}; + +-- _full_text fields are now updated by a trigger when set to NULL +CREATE OR REPLACE FUNCTION populate_full_text_trigger() RETURNS trigger +AS $body$ + BEGIN + IF NEW._full_text IS NOT NULL THEN + RETURN NEW; + END IF; + NEW._full_text := ( + SELECT to_tsvector(string_agg(value, ' ')) + FROM json_each_text(row_to_json(NEW.*)) + WHERE key NOT LIKE '\_%'); + RETURN NEW; + END; +$body$ LANGUAGE plpgsql; +ALTER FUNCTION populate_full_text_trigger() OWNER TO {writeuser}; + +-- migrate existing tables that don't have full text trigger applied +DO $body$ + BEGIN + EXECUTE coalesce( + (SELECT string_agg( + 'CREATE TRIGGER zfulltext BEFORE INSERT OR UPDATE ON ' || + quote_ident(relname) || ' FOR EACH ROW EXECUTE PROCEDURE ' || + 'populate_full_text_trigger();', ' ') + FROM pg_class + LEFT OUTER JOIN pg_trigger AS t + ON t.tgrelid = relname::regclass AND t.tgname = 'zfulltext' + WHERE relkind = 'r'::"char" AND t.tgname IS NULL + AND relnamespace = ( + SELECT oid FROM pg_namespace WHERE nspname='public')), + 'SELECT 1;'); + END; +$body$; + diff --git a/images/ckan/2.9/psql-init/requirements.txt b/images/ckan/2.9/psql-init/requirements.txt new file mode 100644 index 0000000..0662697 --- /dev/null +++ b/images/ckan/2.9/psql-init/requirements.txt @@ -0,0 +1,2 @@ +psycopg2-binary==2.9.3 +sqlalchemy==1.3.5 From 2c021de9f6c89938826e7e3d4c6bfe04e639edf1 Mon Sep 17 00:00:00 2001 From: Nadica Rizova Date: Mon, 3 Jul 2023 09:44:59 +0200 Subject: [PATCH 161/213] added step in workflow to automatic build psql-init image --- .github/workflows/master_merge.yml | 46 ++++++++++++++++++++++++++++ images/ckan/2.9/psql-init/Dockerfile | 4 ++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index 37aaf2d..5461ef1 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -246,3 +246,49 @@ jobs: ghcr.io/keitaroinc/datapusher:${{ steps.datapusher.outputs.IMAGE_TAG }} cache-from: type=local,src=/tmp/.buildx-cache-datapusher cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-datapusher + + build-psql-init: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.CR_PAT }} + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache-psql-init + key: ${{ runner.os }}-buildx-psql-init-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-psql-init + + - name: Get docker tag for psql-init image + id: psql-init + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/psql-init/Dockerfile)" + + - name: Build and push psql-init + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.9/psql-init + file: ./images/ckan/2.9/psql-init/Dockerfile + push: true + tags: | + keitaro/psql-init:${{ steps.psql-init.outputs.IMAGE_TAG }} + ghcr.io/keitaroinc/datapusher:${{ steps.psql-init.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache-psql-init + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-psql-init diff --git a/images/ckan/2.9/psql-init/Dockerfile b/images/ckan/2.9/psql-init/Dockerfile index 9fa996d..e1727ec 100644 --- a/images/ckan/2.9/psql-init/Dockerfile +++ b/images/ckan/2.9/psql-init/Dockerfile @@ -2,6 +2,9 @@ # Start with a lightweight base image FROM python:3.9-alpine +# Used by Github Actions to tag the image with +ENV IMAGE_TAG=0.0.1 + # Set the working directory in the container WORKDIR /srv @@ -14,4 +17,3 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy the rest of the application code to the container COPY psql-init/ . -CMD ["sleep", "1000"] \ No newline at end of file From 47fe1b8cced9ce3ccd64c73b8ca7ea4e1d69ab3f Mon Sep 17 00:00:00 2001 From: Nadica Rizova Date: Mon, 3 Jul 2023 10:34:12 +0200 Subject: [PATCH 162/213] changed image name for psql --- .github/workflows/master_merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index 5461ef1..0a56f4b 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -289,6 +289,6 @@ jobs: push: true tags: | keitaro/psql-init:${{ steps.psql-init.outputs.IMAGE_TAG }} - ghcr.io/keitaroinc/datapusher:${{ steps.psql-init.outputs.IMAGE_TAG }} + ghcr.io/keitaroinc/psql-init:${{ steps.psql-init.outputs.IMAGE_TAG }} cache-from: type=local,src=/tmp/.buildx-cache-psql-init cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-psql-init From 80a4a849f56b4de97c2b87f68c8f8c3b678eaf05 Mon Sep 17 00:00:00 2001 From: Nadica Rizova Date: Mon, 3 Jul 2023 10:41:35 +0200 Subject: [PATCH 163/213] added cmd in Dockerfile --- images/ckan/2.9/psql-init/Dockerfile | 2 ++ images/ckan/2.9/psql-init/psql-init/psql-init.py | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/images/ckan/2.9/psql-init/Dockerfile b/images/ckan/2.9/psql-init/Dockerfile index e1727ec..65fdf5e 100644 --- a/images/ckan/2.9/psql-init/Dockerfile +++ b/images/ckan/2.9/psql-init/Dockerfile @@ -17,3 +17,5 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy the rest of the application code to the container COPY psql-init/ . +CMD ["python", "/srv/psql-init.py"] + diff --git a/images/ckan/2.9/psql-init/psql-init/psql-init.py b/images/ckan/2.9/psql-init/psql-init/psql-init.py index 507b551..6eb25cb 100644 --- a/images/ckan/2.9/psql-init/psql-init/psql-init.py +++ b/images/ckan/2.9/psql-init/psql-init/psql-init.py @@ -248,12 +248,12 @@ def execute_sql_script(ckan_dbp, datastorero_dbp, datastorerw_dbp, script_path): print("SQL script executed successfully.") - # print("CKAN DB User:", ckan_dbp.db_user) - # print("read/write DB User:", datastorerw_dbp.db_user) - # print("read/write DB name:", datastorerw_dbp.db_name) - # print("read/write host:", datastorerw_dbp.db_host) - # print("read DB user:", datastorero_dbp.db_user) - # print("read DB name:", datastorero_dbp.db_name) + print("CKAN DB User:", ckan_dbp.db_user) + print("read/write DB User:", datastorerw_dbp.db_user) + print("read/write DB name:", datastorerw_dbp.db_name) + print("read/write host:", datastorerw_dbp.db_host) + print("read DB user:", datastorero_dbp.db_user) + print("read DB name:", datastorero_dbp.db_name) except psycopg2.Error as e: print(f"Error executing SQL script: {str(e)}") From 53cb2363d6549d33044c148e0a7e1471febbf6d4 Mon Sep 17 00:00:00 2001 From: Nadica Rizova Date: Mon, 3 Jul 2023 10:50:57 +0200 Subject: [PATCH 164/213] changed location of psql-init folder --- .github/workflows/master_merge.yml | 6 +++--- images/{ckan/2.9 => }/psql-init/Dockerfile | 0 images/{ckan/2.9 => }/psql-init/psql-init/psql-init.py | 0 .../{ckan/2.9 => }/psql-init/psql-init/set_permissions.sql | 0 images/{ckan/2.9 => }/psql-init/requirements.txt | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename images/{ckan/2.9 => }/psql-init/Dockerfile (100%) rename images/{ckan/2.9 => }/psql-init/psql-init/psql-init.py (100%) rename images/{ckan/2.9 => }/psql-init/psql-init/set_permissions.sql (100%) rename images/{ckan/2.9 => }/psql-init/requirements.txt (100%) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index 0a56f4b..5d5d03e 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -279,13 +279,13 @@ jobs: - name: Get docker tag for psql-init image id: psql-init run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/psql-init/Dockerfile)" + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/psql-init/Dockerfile)" - name: Build and push psql-init uses: docker/build-push-action@v2 with: - context: ./images/ckan/2.9/psql-init - file: ./images/ckan/2.9/psql-init/Dockerfile + context: ./images/psql-init + file: ./images/psql-init/Dockerfile push: true tags: | keitaro/psql-init:${{ steps.psql-init.outputs.IMAGE_TAG }} diff --git a/images/ckan/2.9/psql-init/Dockerfile b/images/psql-init/Dockerfile similarity index 100% rename from images/ckan/2.9/psql-init/Dockerfile rename to images/psql-init/Dockerfile diff --git a/images/ckan/2.9/psql-init/psql-init/psql-init.py b/images/psql-init/psql-init/psql-init.py similarity index 100% rename from images/ckan/2.9/psql-init/psql-init/psql-init.py rename to images/psql-init/psql-init/psql-init.py diff --git a/images/ckan/2.9/psql-init/psql-init/set_permissions.sql b/images/psql-init/psql-init/set_permissions.sql similarity index 100% rename from images/ckan/2.9/psql-init/psql-init/set_permissions.sql rename to images/psql-init/psql-init/set_permissions.sql diff --git a/images/ckan/2.9/psql-init/requirements.txt b/images/psql-init/requirements.txt similarity index 100% rename from images/ckan/2.9/psql-init/requirements.txt rename to images/psql-init/requirements.txt From 4f770582e92cafe9fdb459a76ebdb4d5de81ec04 Mon Sep 17 00:00:00 2001 From: Nadica Rizova Date: Mon, 3 Jul 2023 11:24:28 +0200 Subject: [PATCH 165/213] pre-checks added for psql init --- .github/workflows/pr_checks.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 4619032..fe7da0f 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -182,3 +182,34 @@ jobs: tags: keitaro/ckandatapusher:${{ steps.datapusher.outputs.IMAGE_TAG }} cache-from: type=local,src=/tmp/.buildx-cache-datapusher cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-datapusher + + build-psql-init: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache-psql-init + key: ${{ runner.os }}-buildx-psql-init-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-psql-init + + - name: Get docker tag for psql-init image + id: psql-init + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/psql-init/Dockerfile)" + + - name: Build psql-init + uses: docker/build-push-action@v2 + with: + context: ./images/psgl-init + file: ./images/psql-init/Dockerfile + push: false + tags: keitaro/psql-init:${{ steps.psql-init.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache-psql-init + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-psql-init \ No newline at end of file From 640b0fa09c07e12d0ec06203ebdbea38ae08766b Mon Sep 17 00:00:00 2001 From: Nadica Rizova Date: Mon, 3 Jul 2023 11:28:46 +0200 Subject: [PATCH 166/213] syntax changed psql --- .github/workflows/pr_checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index fe7da0f..c37d6e9 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -207,7 +207,7 @@ jobs: - name: Build psql-init uses: docker/build-push-action@v2 with: - context: ./images/psgl-init + context: ./images/psql-init file: ./images/psql-init/Dockerfile push: false tags: keitaro/psql-init:${{ steps.psql-init.outputs.IMAGE_TAG }} From 4d4c4d57a7fa44e2561892d33841e488a85bd186 Mon Sep 17 00:00:00 2001 From: Nadica Rizova Date: Wed, 5 Jul 2023 12:59:37 +0200 Subject: [PATCH 167/213] uwsgi.conf for ckan 2.9 --- .../ckan/2.9/setup/app/basic-auth-uwsgi.conf | 2 + images/ckan/2.9/setup/app/start_ckan.sh | 4 +- images/ckan/2.9/setup/app/uwsgi.conf | 38 ++++++++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 images/ckan/2.9/setup/app/basic-auth-uwsgi.conf diff --git a/images/ckan/2.9/setup/app/basic-auth-uwsgi.conf b/images/ckan/2.9/setup/app/basic-auth-uwsgi.conf new file mode 100644 index 0000000..6321d6d --- /dev/null +++ b/images/ckan/2.9/setup/app/basic-auth-uwsgi.conf @@ -0,0 +1,2 @@ +[uwsgi] +route = ^(?!/api).*$ basicauth:Restricted,/srv/app/.htpasswd diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index 9cd7d24..23747fb 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -31,7 +31,7 @@ then fi echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" -UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid ckan --gid ckan --http :5000 --master --enable-threads --wsgi-file /srv/app/wsgi.py --module wsgi:application --lazy-apps --gevent 2000 -p ${UWSGI_PROC_NO:-2} -L --gevent-early-monkey-patch --vacuum --harakiri 50 --callable application" +uwsgi --ini uwsgi.conf -p ${UWSGI_PROC_NO:-2} # Run the prerun script to init CKAN and create the default admin user python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } @@ -62,7 +62,7 @@ then # Generate htpasswd file for basicauth htpasswd -d -b -c /srv/app/.htpasswd $HTPASSWD_USER $HTPASSWD_PASSWORD # Start uwsgi with basicauth - uwsgi --ini /srv/app/uwsgi.conf --pcre-jit $UWSGI_OPTS + uwsgi --ini /srv/app/basic-auth-uwsgi.conf --pcre-jit $UWSGI_OPTS else echo "Missing HTPASSWD_USER or HTPASSWD_PASSWORD environment variables. Exiting..." exit 1 diff --git a/images/ckan/2.9/setup/app/uwsgi.conf b/images/ckan/2.9/setup/app/uwsgi.conf index 6321d6d..b2b2e1d 100644 --- a/images/ckan/2.9/setup/app/uwsgi.conf +++ b/images/ckan/2.9/setup/app/uwsgi.conf @@ -1,2 +1,38 @@ [uwsgi] -route = ^(?!/api).*$ basicauth:Restricted,/srv/app/.htpasswd +socket = /tmp/uwsgi.sock +uid = ckan +gid = ckan +http = :5000 + +master = true +enable-threads = true +lazy-apps = true +gevent-early-monkey-patch = true +vacuum = true +single-interpreter= true +die-on-term = true +need-app = true +auto-procname = true + +wsgi-file = /srv/app/wsgi.py +module = wsgi:application +gevent = 2000 +logto = /var/log/uwsgi/uwsgi.log +callable = application +paste = config:/srv/app/production.ini +paste-logger = /srv/app/production.ini + +post-buffering = 1 +buffer-size= 12288 +max-requests = 3000 +max-worker-lifetime = 3600 +reload-on-rss = 4096 +worker-reload-mercy = 60 +socket-timeout = 300 +queue = 1000 +queue-blocksize = 204800 +static-gzip-all = true +listen = 1000 +http-timeout = 1000 +http-headers-timeout = 1000 +http-connect-timeout = 1000 \ No newline at end of file From 4c702ba404a2f6f3b4831c0ba66dbbeb8fb70332 Mon Sep 17 00:00:00 2001 From: Nadica Rizova Date: Fri, 7 Jul 2023 09:22:24 +0200 Subject: [PATCH 168/213] uwsgi.conf and basic-auth-uwsgi.conf changed --- .../ckan/2.9/setup/app/basic-auth-uwsgi.conf | 38 +++++++++++++++++++ images/ckan/2.9/setup/app/start_ckan.sh | 8 ++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/images/ckan/2.9/setup/app/basic-auth-uwsgi.conf b/images/ckan/2.9/setup/app/basic-auth-uwsgi.conf index 6321d6d..392e4e2 100644 --- a/images/ckan/2.9/setup/app/basic-auth-uwsgi.conf +++ b/images/ckan/2.9/setup/app/basic-auth-uwsgi.conf @@ -1,2 +1,40 @@ [uwsgi] route = ^(?!/api).*$ basicauth:Restricted,/srv/app/.htpasswd + +socket = /tmp/uwsgi.sock +uid = ckan +gid = ckan +http = :5000 + +master = true +enable-threads = true +lazy-apps = true +gevent-early-monkey-patch = true +vacuum = true +single-interpreter= true +die-on-term = true +need-app = true +auto-procname = true + +wsgi-file = /srv/app/wsgi.py +module = wsgi:application +gevent = 2000 +logto = /var/log/uwsgi/uwsgi.log +callable = application +paste = config:/srv/app/production.ini +paste-logger = /srv/app/production.ini + +post-buffering = 1 +buffer-size= 12288 +max-requests = 3000 +max-worker-lifetime = 3600 +reload-on-rss = 4096 +worker-reload-mercy = 60 +socket-timeout = 300 +queue = 1000 +queue-blocksize = 204800 +static-gzip-all = true +listen = 1000 +http-timeout = 1000 +http-headers-timeout = 1000 +http-connect-timeout = 1000 \ No newline at end of file diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index 23747fb..58dffd7 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -30,9 +30,6 @@ then ckan config-tool $APP_DIR/production.ini "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" fi -echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" -uwsgi --ini uwsgi.conf -p ${UWSGI_PROC_NO:-2} - # Run the prerun script to init CKAN and create the default admin user python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } @@ -62,14 +59,15 @@ then # Generate htpasswd file for basicauth htpasswd -d -b -c /srv/app/.htpasswd $HTPASSWD_USER $HTPASSWD_PASSWORD # Start uwsgi with basicauth - uwsgi --ini /srv/app/basic-auth-uwsgi.conf --pcre-jit $UWSGI_OPTS + uwsgi --ini /srv/app/basic-auth-uwsgi.conf -p ${UWSGI_PROC_NO:-2} --pcre-jit else echo "Missing HTPASSWD_USER or HTPASSWD_PASSWORD environment variables. Exiting..." exit 1 fi else # Start uwsgi - uwsgi $UWSGI_OPTS + echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" + uwsgi --ini /srv/app/uwsgi.conf -p ${UWSGI_PROC_NO:-2} fi else echo "[prerun] failed...not starting CKAN." From 9b94565ca920c452eafb52bc311bec8cc25bd62e Mon Sep 17 00:00:00 2001 From: Nadica Rizova Date: Fri, 7 Jul 2023 13:26:30 +0200 Subject: [PATCH 169/213] logto in uwsgi removed --- images/ckan/2.9/setup/app/basic-auth-uwsgi.conf | 1 - images/ckan/2.9/setup/app/uwsgi.conf | 1 - 2 files changed, 2 deletions(-) diff --git a/images/ckan/2.9/setup/app/basic-auth-uwsgi.conf b/images/ckan/2.9/setup/app/basic-auth-uwsgi.conf index 392e4e2..e7988b1 100644 --- a/images/ckan/2.9/setup/app/basic-auth-uwsgi.conf +++ b/images/ckan/2.9/setup/app/basic-auth-uwsgi.conf @@ -19,7 +19,6 @@ auto-procname = true wsgi-file = /srv/app/wsgi.py module = wsgi:application gevent = 2000 -logto = /var/log/uwsgi/uwsgi.log callable = application paste = config:/srv/app/production.ini paste-logger = /srv/app/production.ini diff --git a/images/ckan/2.9/setup/app/uwsgi.conf b/images/ckan/2.9/setup/app/uwsgi.conf index b2b2e1d..c0974de 100644 --- a/images/ckan/2.9/setup/app/uwsgi.conf +++ b/images/ckan/2.9/setup/app/uwsgi.conf @@ -17,7 +17,6 @@ auto-procname = true wsgi-file = /srv/app/wsgi.py module = wsgi:application gevent = 2000 -logto = /var/log/uwsgi/uwsgi.log callable = application paste = config:/srv/app/production.ini paste-logger = /srv/app/production.ini From 909359ca674182ca9149dade8de9a6cd2d90529b Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Thu, 20 Jul 2023 11:00:03 +0200 Subject: [PATCH 170/213] updated alpine version of 2.9 --- images/ckan/2.9/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 8d075c8..41e0433 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.13.7 as ckanbuild +FROM alpine:3.18.2 as ckanbuild # Used by Github Actions to tag the image with ENV IMAGE_TAG=2.9.9 @@ -66,7 +66,7 @@ RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.12.0 greenlet==1.1.3 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.13.7 as extbuild +FROM alpine:3.18.2 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -101,7 +101,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.13.7 +FROM alpine:3.18.2 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan From 3944962ee20c9b9c5e329a57fdae4fcbe03c4198 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Thu, 20 Jul 2023 13:32:42 +0200 Subject: [PATCH 171/213] fixed cyton dep --- images/ckan/2.9/Dockerfile | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 41e0433..142ba85 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.18.2 as ckanbuild +FROM alpine:3.16 as ckanbuild # Used by Github Actions to tag the image with ENV IMAGE_TAG=2.9.9 @@ -59,14 +59,18 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh # Apply patches RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git + +#### PATCH SINCE CYTON UPDATED TO 3.0.0 ### +RUN sed -i 's/pyyaml==5.4.1/pyyaml>=6.0.1/g' ckan/requirements.txt + +# RUN pip-compile ckan/requirements.in RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.12.0 greenlet==1.1.3 - ########################### ### Default-Extensions #### ########################### -FROM alpine:3.18.2 as extbuild +FROM alpine:3.16 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -101,7 +105,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.18.2 +FROM alpine:3.16 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan From b7c48fe252c7a715760a4450a5d0f4a0602bf2a2 Mon Sep 17 00:00:00 2001 From: pdonchev1 <131678350+pdonchev1@users.noreply.github.com> Date: Thu, 20 Jul 2023 14:03:03 +0200 Subject: [PATCH 172/213] Update Dockerfile apply_ckan_patches update --- images/ckan/2.9/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 142ba85..d7b4983 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -57,7 +57,7 @@ RUN pip install -e git+${GIT_URL}@${GIT_BRANCH}#egg=ckan COPY ./patches ${SRC_DIR}/patches COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh # Apply patches -RUN ${SRC_DIR}/apply_ckan_patches.sh +RUN cd ${SRC_DIR} && ls -lah ${SRC_DIR} && ash ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git #### PATCH SINCE CYTON UPDATED TO 3.0.0 ### From 8878d03ec3bc1a197f604949c64fdb9374dbb223 Mon Sep 17 00:00:00 2001 From: Petar Date: Thu, 20 Jul 2023 15:38:51 +0200 Subject: [PATCH 173/213] update for python3.10 error --- images/ckan/2.9/Dockerfile | 2 + images/ckan/2.9/__init__.py | 1560 +++++++++++++++++++++++++++++++++++ 2 files changed, 1562 insertions(+) create mode 100644 images/ckan/2.9/__init__.py diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index d7b4983..33ae654 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -154,6 +154,8 @@ RUN addgroup -g 92 -S ckan && \ adduser -u 92 -h /srv/app -H -D -S -G ckan ckan WORKDIR ${CKAN_DIR} +RUN ls -lah /usr/lib/python3.10/collections/ && cat /usr/lib/python3.10/collections/__init__.py +COPY __init__.py /usr/lib/python3.10/collections/__init__.py # Install CKAN RUN pip install -e /srv/app/src/ckan && \ diff --git a/images/ckan/2.9/__init__.py b/images/ckan/2.9/__init__.py new file mode 100644 index 0000000..c418f07 --- /dev/null +++ b/images/ckan/2.9/__init__.py @@ -0,0 +1,1560 @@ +'''This module implements specialized container datatypes providing +alternatives to Python's general purpose built-in containers, dict, +list, set, and tuple. + +* namedtuple factory function for creating tuple subclasses with named fields +* deque list-like container with fast appends and pops on either end +* ChainMap dict-like class for creating a single view of multiple mappings +* Counter dict subclass for counting hashable objects +* OrderedDict dict subclass that remembers the order entries were added +* defaultdict dict subclass that calls a factory function to supply missing values +* UserDict wrapper around dictionary objects for easier dict subclassing +* UserList wrapper around list objects for easier list subclassing +* UserString wrapper around string objects for easier string subclassing + +''' + +__all__ = [ + 'ChainMap', + 'Counter', + 'OrderedDict', + 'UserDict', + 'UserList', + 'UserString', + 'defaultdict', + 'deque', + 'namedtuple', +] + +from _collections_abc import Mapping +from _collections_abc import MutableMapping +from _collections_abc import Sequence + +import _collections_abc +import sys as _sys + +from itertools import chain as _chain +from itertools import repeat as _repeat +from itertools import starmap as _starmap +from keyword import iskeyword as _iskeyword +from operator import eq as _eq +from operator import itemgetter as _itemgetter +from reprlib import recursive_repr as _recursive_repr +from _weakref import proxy as _proxy + +try: + from _collections import deque +except ImportError: + pass +else: + _collections_abc.MutableSequence.register(deque) + +try: + from _collections import defaultdict +except ImportError: + pass + + +################################################################################ +### OrderedDict +################################################################################ + +class _OrderedDictKeysView(_collections_abc.KeysView): + + def __reversed__(self): + yield from reversed(self._mapping) + +class _OrderedDictItemsView(_collections_abc.ItemsView): + + def __reversed__(self): + for key in reversed(self._mapping): + yield (key, self._mapping[key]) + +class _OrderedDictValuesView(_collections_abc.ValuesView): + + def __reversed__(self): + for key in reversed(self._mapping): + yield self._mapping[key] + +class _Link(object): + __slots__ = 'prev', 'next', 'key', '__weakref__' + +class OrderedDict(dict): + 'Dictionary that remembers insertion order' + # An inherited dict maps keys to values. + # The inherited dict provides __getitem__, __len__, __contains__, and get. + # The remaining methods are order-aware. + # Big-O running times for all methods are the same as regular dictionaries. + + # The internal self.__map dict maps keys to links in a doubly linked list. + # The circular doubly linked list starts and ends with a sentinel element. + # The sentinel element never gets deleted (this simplifies the algorithm). + # The sentinel is in self.__hardroot with a weakref proxy in self.__root. + # The prev links are weakref proxies (to prevent circular references). + # Individual links are kept alive by the hard reference in self.__map. + # Those hard references disappear when a key is deleted from an OrderedDict. + + def __init__(self, other=(), /, **kwds): + '''Initialize an ordered dictionary. The signature is the same as + regular dictionaries. Keyword argument order is preserved. + ''' + try: + self.__root + except AttributeError: + self.__hardroot = _Link() + self.__root = root = _proxy(self.__hardroot) + root.prev = root.next = root + self.__map = {} + self.__update(other, **kwds) + + def __setitem__(self, key, value, + dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link): + 'od.__setitem__(i, y) <==> od[i]=y' + # Setting a new item creates a new link at the end of the linked list, + # and the inherited dictionary is updated with the new key/value pair. + if key not in self: + self.__map[key] = link = Link() + root = self.__root + last = root.prev + link.prev, link.next, link.key = last, root, key + last.next = link + root.prev = proxy(link) + dict_setitem(self, key, value) + + def __delitem__(self, key, dict_delitem=dict.__delitem__): + 'od.__delitem__(y) <==> del od[y]' + # Deleting an existing item uses self.__map to find the link which gets + # removed by updating the links in the predecessor and successor nodes. + dict_delitem(self, key) + link = self.__map.pop(key) + link_prev = link.prev + link_next = link.next + link_prev.next = link_next + link_next.prev = link_prev + link.prev = None + link.next = None + + def __iter__(self): + 'od.__iter__() <==> iter(od)' + # Traverse the linked list in order. + root = self.__root + curr = root.next + while curr is not root: + yield curr.key + curr = curr.next + + def __reversed__(self): + 'od.__reversed__() <==> reversed(od)' + # Traverse the linked list in reverse order. + root = self.__root + curr = root.prev + while curr is not root: + yield curr.key + curr = curr.prev + + def clear(self): + 'od.clear() -> None. Remove all items from od.' + root = self.__root + root.prev = root.next = root + self.__map.clear() + dict.clear(self) + + def popitem(self, last=True): + '''Remove and return a (key, value) pair from the dictionary. + + Pairs are returned in LIFO order if last is true or FIFO order if false. + ''' + if not self: + raise KeyError('dictionary is empty') + root = self.__root + if last: + link = root.prev + link_prev = link.prev + link_prev.next = root + root.prev = link_prev + else: + link = root.next + link_next = link.next + root.next = link_next + link_next.prev = root + key = link.key + del self.__map[key] + value = dict.pop(self, key) + return key, value + + def move_to_end(self, key, last=True): + '''Move an existing element to the end (or beginning if last is false). + + Raise KeyError if the element does not exist. + ''' + link = self.__map[key] + link_prev = link.prev + link_next = link.next + soft_link = link_next.prev + link_prev.next = link_next + link_next.prev = link_prev + root = self.__root + if last: + last = root.prev + link.prev = last + link.next = root + root.prev = soft_link + last.next = link + else: + first = root.next + link.prev = root + link.next = first + first.prev = soft_link + root.next = link + + def __sizeof__(self): + sizeof = _sys.getsizeof + n = len(self) + 1 # number of links including root + size = sizeof(self.__dict__) # instance dictionary + size += sizeof(self.__map) * 2 # internal dict and inherited dict + size += sizeof(self.__hardroot) * n # link objects + size += sizeof(self.__root) * n # proxy objects + return size + + update = __update = _collections_abc.MutableMapping.update + + def keys(self): + "D.keys() -> a set-like object providing a view on D's keys" + return _OrderedDictKeysView(self) + + def items(self): + "D.items() -> a set-like object providing a view on D's items" + return _OrderedDictItemsView(self) + + def values(self): + "D.values() -> an object providing a view on D's values" + return _OrderedDictValuesView(self) + + __ne__ = _collections_abc.MutableMapping.__ne__ + + __marker = object() + + def pop(self, key, default=__marker): + '''od.pop(k[,d]) -> v, remove specified key and return the corresponding + value. If key is not found, d is returned if given, otherwise KeyError + is raised. + + ''' + if key in self: + result = self[key] + del self[key] + return result + if default is self.__marker: + raise KeyError(key) + return default + + def setdefault(self, key, default=None): + '''Insert key with a value of default if key is not in the dictionary. + + Return the value for key if key is in the dictionary, else default. + ''' + if key in self: + return self[key] + self[key] = default + return default + + @_recursive_repr() + def __repr__(self): + 'od.__repr__() <==> repr(od)' + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self.items())) + + def __reduce__(self): + 'Return state information for pickling' + inst_dict = vars(self).copy() + for k in vars(OrderedDict()): + inst_dict.pop(k, None) + return self.__class__, (), inst_dict or None, None, iter(self.items()) + + def copy(self): + 'od.copy() -> a shallow copy of od' + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + '''Create a new ordered dictionary with keys from iterable and values set to value. + ''' + self = cls() + for key in iterable: + self[key] = value + return self + + def __eq__(self, other): + '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive + while comparison to a regular mapping is order-insensitive. + + ''' + if isinstance(other, OrderedDict): + return dict.__eq__(self, other) and all(map(_eq, self, other)) + return dict.__eq__(self, other) + + def __ior__(self, other): + self.update(other) + return self + + def __or__(self, other): + if not isinstance(other, dict): + return NotImplemented + new = self.__class__(self) + new.update(other) + return new + + def __ror__(self, other): + if not isinstance(other, dict): + return NotImplemented + new = self.__class__(other) + new.update(self) + return new + + +try: + from _collections import OrderedDict +except ImportError: + # Leave the pure Python version in place. + pass + + +################################################################################ +### namedtuple +################################################################################ + +try: + from _collections import _tuplegetter +except ImportError: + _tuplegetter = lambda index, doc: property(_itemgetter(index), doc=doc) + +def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None): + """Returns a new subclass of tuple with named fields. + + >>> Point = namedtuple('Point', ['x', 'y']) + >>> Point.__doc__ # docstring for the new class + 'Point(x, y)' + >>> p = Point(11, y=22) # instantiate with positional args or keywords + >>> p[0] + p[1] # indexable like a plain tuple + 33 + >>> x, y = p # unpack like a regular tuple + >>> x, y + (11, 22) + >>> p.x + p.y # fields also accessible by name + 33 + >>> d = p._asdict() # convert to a dictionary + >>> d['x'] + 11 + >>> Point(**d) # convert from a dictionary + Point(x=11, y=22) + >>> p._replace(x=100) # _replace() is like str.replace() but targets named fields + Point(x=100, y=22) + + """ + + # Validate the field names. At the user's option, either generate an error + # message or automatically replace the field name with a valid name. + if isinstance(field_names, str): + field_names = field_names.replace(',', ' ').split() + field_names = list(map(str, field_names)) + typename = _sys.intern(str(typename)) + + if rename: + seen = set() + for index, name in enumerate(field_names): + if (not name.isidentifier() + or _iskeyword(name) + or name.startswith('_') + or name in seen): + field_names[index] = f'_{index}' + seen.add(name) + + for name in [typename] + field_names: + if type(name) is not str: + raise TypeError('Type names and field names must be strings') + if not name.isidentifier(): + raise ValueError('Type names and field names must be valid ' + f'identifiers: {name!r}') + if _iskeyword(name): + raise ValueError('Type names and field names cannot be a ' + f'keyword: {name!r}') + + seen = set() + for name in field_names: + if name.startswith('_') and not rename: + raise ValueError('Field names cannot start with an underscore: ' + f'{name!r}') + if name in seen: + raise ValueError(f'Encountered duplicate field name: {name!r}') + seen.add(name) + + field_defaults = {} + if defaults is not None: + defaults = tuple(defaults) + if len(defaults) > len(field_names): + raise TypeError('Got more default values than field names') + field_defaults = dict(reversed(list(zip(reversed(field_names), + reversed(defaults))))) + + # Variables used in the methods and docstrings + field_names = tuple(map(_sys.intern, field_names)) + num_fields = len(field_names) + arg_list = ', '.join(field_names) + if num_fields == 1: + arg_list += ',' + repr_fmt = '(' + ', '.join(f'{name}=%r' for name in field_names) + ')' + tuple_new = tuple.__new__ + _dict, _tuple, _len, _map, _zip = dict, tuple, len, map, zip + + # Create all the named tuple methods to be added to the class namespace + + namespace = { + '_tuple_new': tuple_new, + '__builtins__': {}, + '__name__': f'namedtuple_{typename}', + } + code = f'lambda _cls, {arg_list}: _tuple_new(_cls, ({arg_list}))' + __new__ = eval(code, namespace) + __new__.__name__ = '__new__' + __new__.__doc__ = f'Create new instance of {typename}({arg_list})' + if defaults is not None: + __new__.__defaults__ = defaults + + @classmethod + def _make(cls, iterable): + result = tuple_new(cls, iterable) + if _len(result) != num_fields: + raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') + return result + + _make.__func__.__doc__ = (f'Make a new {typename} object from a sequence ' + 'or iterable') + + def _replace(self, /, **kwds): + result = self._make(_map(kwds.pop, field_names, self)) + if kwds: + raise ValueError(f'Got unexpected field names: {list(kwds)!r}') + return result + + _replace.__doc__ = (f'Return a new {typename} object replacing specified ' + 'fields with new values') + + def __repr__(self): + 'Return a nicely formatted representation string' + return self.__class__.__name__ + repr_fmt % self + + def _asdict(self): + 'Return a new dict which maps field names to their values.' + return _dict(_zip(self._fields, self)) + + def __getnewargs__(self): + 'Return self as a plain tuple. Used by copy and pickle.' + return _tuple(self) + + # Modify function metadata to help with introspection and debugging + for method in ( + __new__, + _make.__func__, + _replace, + __repr__, + _asdict, + __getnewargs__, + ): + method.__qualname__ = f'{typename}.{method.__name__}' + + # Build-up the class namespace dictionary + # and use type() to build the result class + class_namespace = { + '__doc__': f'{typename}({arg_list})', + '__slots__': (), + '_fields': field_names, + '_field_defaults': field_defaults, + '__new__': __new__, + '_make': _make, + '_replace': _replace, + '__repr__': __repr__, + '_asdict': _asdict, + '__getnewargs__': __getnewargs__, + '__match_args__': field_names, + } + for index, name in enumerate(field_names): + doc = _sys.intern(f'Alias for field number {index}') + class_namespace[name] = _tuplegetter(index, doc) + + result = type(typename, (tuple,), class_namespace) + + # For pickling to work, the __module__ variable needs to be set to the frame + # where the named tuple is created. Bypass this step in environments where + # sys._getframe is not defined (Jython for example) or sys._getframe is not + # defined for arguments greater than 0 (IronPython), or where the user has + # specified a particular module. + if module is None: + try: + module = _sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + pass + if module is not None: + result.__module__ = module + + return result + + +######################################################################## +### Counter +######################################################################## + +def _count_elements(mapping, iterable): + 'Tally elements from the iterable.' + mapping_get = mapping.get + for elem in iterable: + mapping[elem] = mapping_get(elem, 0) + 1 + +try: # Load C helper function if available + from _collections import _count_elements +except ImportError: + pass + +class Counter(dict): + '''Dict subclass for counting hashable items. Sometimes called a bag + or multiset. Elements are stored as dictionary keys and their counts + are stored as dictionary values. + + >>> c = Counter('abcdeabcdabcaba') # count elements from a string + + >>> c.most_common(3) # three most common elements + [('a', 5), ('b', 4), ('c', 3)] + >>> sorted(c) # list all unique elements + ['a', 'b', 'c', 'd', 'e'] + >>> ''.join(sorted(c.elements())) # list elements with repetitions + 'aaaaabbbbcccdde' + >>> sum(c.values()) # total of all counts + 15 + + >>> c['a'] # count of letter 'a' + 5 + >>> for elem in 'shazam': # update counts from an iterable + ... c[elem] += 1 # by adding 1 to each element's count + >>> c['a'] # now there are seven 'a' + 7 + >>> del c['b'] # remove all 'b' + >>> c['b'] # now there are zero 'b' + 0 + + >>> d = Counter('simsalabim') # make another counter + >>> c.update(d) # add in the second counter + >>> c['a'] # now there are nine 'a' + 9 + + >>> c.clear() # empty the counter + >>> c + Counter() + + Note: If a count is set to zero or reduced to zero, it will remain + in the counter until the entry is deleted or the counter is cleared: + + >>> c = Counter('aaabbc') + >>> c['b'] -= 2 # reduce the count of 'b' by two + >>> c.most_common() # 'b' is still in, but its count is zero + [('a', 3), ('c', 1), ('b', 0)] + + ''' + # References: + # http://en.wikipedia.org/wiki/Multiset + # http://www.gnu.org/software/smalltalk/manual-base/html_node/Bag.html + # http://www.demo2s.com/Tutorial/Cpp/0380__set-multiset/Catalog0380__set-multiset.htm + # http://code.activestate.com/recipes/259174/ + # Knuth, TAOCP Vol. II section 4.6.3 + + def __init__(self, iterable=None, /, **kwds): + '''Create a new, empty Counter object. And if given, count elements + from an input iterable. Or, initialize the count from another mapping + of elements to their counts. + + >>> c = Counter() # a new, empty counter + >>> c = Counter('gallahad') # a new counter from an iterable + >>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping + >>> c = Counter(a=4, b=2) # a new counter from keyword args + + ''' + super().__init__() + self.update(iterable, **kwds) + + def __missing__(self, key): + 'The count of elements not in the Counter is zero.' + # Needed so that self[missing_item] does not raise KeyError + return 0 + + def total(self): + 'Sum of the counts' + return sum(self.values()) + + def most_common(self, n=None): + '''List the n most common elements and their counts from the most + common to the least. If n is None, then list all element counts. + + >>> Counter('abracadabra').most_common(3) + [('a', 5), ('b', 2), ('r', 2)] + + ''' + # Emulate Bag.sortedByCount from Smalltalk + if n is None: + return sorted(self.items(), key=_itemgetter(1), reverse=True) + + # Lazy import to speedup Python startup time + import heapq + return heapq.nlargest(n, self.items(), key=_itemgetter(1)) + + def elements(self): + '''Iterator over elements repeating each as many times as its count. + + >>> c = Counter('ABCABC') + >>> sorted(c.elements()) + ['A', 'A', 'B', 'B', 'C', 'C'] + + # Knuth's example for prime factors of 1836: 2**2 * 3**3 * 17**1 + >>> prime_factors = Counter({2: 2, 3: 3, 17: 1}) + >>> product = 1 + >>> for factor in prime_factors.elements(): # loop over factors + ... product *= factor # and multiply them + >>> product + 1836 + + Note, if an element's count has been set to zero or is a negative + number, elements() will ignore it. + + ''' + # Emulate Bag.do from Smalltalk and Multiset.begin from C++. + return _chain.from_iterable(_starmap(_repeat, self.items())) + + # Override dict methods where necessary + + @classmethod + def fromkeys(cls, iterable, v=None): + # There is no equivalent method for counters because the semantics + # would be ambiguous in cases such as Counter.fromkeys('aaabbc', v=2). + # Initializing counters to zero values isn't necessary because zero + # is already the default value for counter lookups. Initializing + # to one is easily accomplished with Counter(set(iterable)). For + # more exotic cases, create a dictionary first using a dictionary + # comprehension or dict.fromkeys(). + raise NotImplementedError( + 'Counter.fromkeys() is undefined. Use Counter(iterable) instead.') + + def update(self, iterable=None, /, **kwds): + '''Like dict.update() but add counts instead of replacing them. + + Source can be an iterable, a dictionary, or another Counter instance. + + >>> c = Counter('which') + >>> c.update('witch') # add elements from another iterable + >>> d = Counter('watch') + >>> c.update(d) # add elements from another counter + >>> c['h'] # four 'h' in which, witch, and watch + 4 + + ''' + # The regular dict.update() operation makes no sense here because the + # replace behavior results in the some of original untouched counts + # being mixed-in with all of the other counts for a mismash that + # doesn't have a straight-forward interpretation in most counting + # contexts. Instead, we implement straight-addition. Both the inputs + # and outputs are allowed to contain zero and negative counts. + + if iterable is not None: + if isinstance(iterable, _collections_abc.Mapping): + if self: + self_get = self.get + for elem, count in iterable.items(): + self[elem] = count + self_get(elem, 0) + else: + # fast path when counter is empty + super().update(iterable) + else: + _count_elements(self, iterable) + if kwds: + self.update(kwds) + + def subtract(self, iterable=None, /, **kwds): + '''Like dict.update() but subtracts counts instead of replacing them. + Counts can be reduced below zero. Both the inputs and outputs are + allowed to contain zero and negative counts. + + Source can be an iterable, a dictionary, or another Counter instance. + + >>> c = Counter('which') + >>> c.subtract('witch') # subtract elements from another iterable + >>> c.subtract(Counter('watch')) # subtract elements from another counter + >>> c['h'] # 2 in which, minus 1 in witch, minus 1 in watch + 0 + >>> c['w'] # 1 in which, minus 1 in witch, minus 1 in watch + -1 + + ''' + if iterable is not None: + self_get = self.get + if isinstance(iterable, _collections_abc.Mapping): + for elem, count in iterable.items(): + self[elem] = self_get(elem, 0) - count + else: + for elem in iterable: + self[elem] = self_get(elem, 0) - 1 + if kwds: + self.subtract(kwds) + + def copy(self): + 'Return a shallow copy.' + return self.__class__(self) + + def __reduce__(self): + return self.__class__, (dict(self),) + + def __delitem__(self, elem): + 'Like dict.__delitem__() but does not raise KeyError for missing values.' + if elem in self: + super().__delitem__(elem) + + def __eq__(self, other): + 'True if all counts agree. Missing counts are treated as zero.' + if not isinstance(other, Counter): + return NotImplemented + return all(self[e] == other[e] for c in (self, other) for e in c) + + def __ne__(self, other): + 'True if any counts disagree. Missing counts are treated as zero.' + if not isinstance(other, Counter): + return NotImplemented + return not self == other + + def __le__(self, other): + 'True if all counts in self are a subset of those in other.' + if not isinstance(other, Counter): + return NotImplemented + return all(self[e] <= other[e] for c in (self, other) for e in c) + + def __lt__(self, other): + 'True if all counts in self are a proper subset of those in other.' + if not isinstance(other, Counter): + return NotImplemented + return self <= other and self != other + + def __ge__(self, other): + 'True if all counts in self are a superset of those in other.' + if not isinstance(other, Counter): + return NotImplemented + return all(self[e] >= other[e] for c in (self, other) for e in c) + + def __gt__(self, other): + 'True if all counts in self are a proper superset of those in other.' + if not isinstance(other, Counter): + return NotImplemented + return self >= other and self != other + + def __repr__(self): + if not self: + return f'{self.__class__.__name__}()' + try: + # dict() preserves the ordering returned by most_common() + d = dict(self.most_common()) + except TypeError: + # handle case where values are not orderable + d = dict(self) + return f'{self.__class__.__name__}({d!r})' + + # Multiset-style mathematical operations discussed in: + # Knuth TAOCP Volume II section 4.6.3 exercise 19 + # and at http://en.wikipedia.org/wiki/Multiset + # + # Outputs guaranteed to only include positive counts. + # + # To strip negative and zero counts, add-in an empty counter: + # c += Counter() + # + # Results are ordered according to when an element is first + # encountered in the left operand and then by the order + # encountered in the right operand. + # + # When the multiplicities are all zero or one, multiset operations + # are guaranteed to be equivalent to the corresponding operations + # for regular sets. + # Given counter multisets such as: + # cp = Counter(a=1, b=0, c=1) + # cq = Counter(c=1, d=0, e=1) + # The corresponding regular sets would be: + # sp = {'a', 'c'} + # sq = {'c', 'e'} + # All of the following relations would hold: + # set(cp + cq) == sp | sq + # set(cp - cq) == sp - sq + # set(cp | cq) == sp | sq + # set(cp & cq) == sp & sq + # (cp == cq) == (sp == sq) + # (cp != cq) == (sp != sq) + # (cp <= cq) == (sp <= sq) + # (cp < cq) == (sp < sq) + # (cp >= cq) == (sp >= sq) + # (cp > cq) == (sp > sq) + + def __add__(self, other): + '''Add counts from two counters. + + >>> Counter('abbb') + Counter('bcc') + Counter({'b': 4, 'c': 2, 'a': 1}) + + ''' + if not isinstance(other, Counter): + return NotImplemented + result = Counter() + for elem, count in self.items(): + newcount = count + other[elem] + if newcount > 0: + result[elem] = newcount + for elem, count in other.items(): + if elem not in self and count > 0: + result[elem] = count + return result + + def __sub__(self, other): + ''' Subtract count, but keep only results with positive counts. + + >>> Counter('abbbc') - Counter('bccd') + Counter({'b': 2, 'a': 1}) + + ''' + if not isinstance(other, Counter): + return NotImplemented + result = Counter() + for elem, count in self.items(): + newcount = count - other[elem] + if newcount > 0: + result[elem] = newcount + for elem, count in other.items(): + if elem not in self and count < 0: + result[elem] = 0 - count + return result + + def __or__(self, other): + '''Union is the maximum of value in either of the input counters. + + >>> Counter('abbb') | Counter('bcc') + Counter({'b': 3, 'c': 2, 'a': 1}) + + ''' + if not isinstance(other, Counter): + return NotImplemented + result = Counter() + for elem, count in self.items(): + other_count = other[elem] + newcount = other_count if count < other_count else count + if newcount > 0: + result[elem] = newcount + for elem, count in other.items(): + if elem not in self and count > 0: + result[elem] = count + return result + + def __and__(self, other): + ''' Intersection is the minimum of corresponding counts. + + >>> Counter('abbb') & Counter('bcc') + Counter({'b': 1}) + + ''' + if not isinstance(other, Counter): + return NotImplemented + result = Counter() + for elem, count in self.items(): + other_count = other[elem] + newcount = count if count < other_count else other_count + if newcount > 0: + result[elem] = newcount + return result + + def __pos__(self): + 'Adds an empty counter, effectively stripping negative and zero counts' + result = Counter() + for elem, count in self.items(): + if count > 0: + result[elem] = count + return result + + def __neg__(self): + '''Subtracts from an empty counter. Strips positive and zero counts, + and flips the sign on negative counts. + + ''' + result = Counter() + for elem, count in self.items(): + if count < 0: + result[elem] = 0 - count + return result + + def _keep_positive(self): + '''Internal method to strip elements with a negative or zero count''' + nonpositive = [elem for elem, count in self.items() if not count > 0] + for elem in nonpositive: + del self[elem] + return self + + def __iadd__(self, other): + '''Inplace add from another counter, keeping only positive counts. + + >>> c = Counter('abbb') + >>> c += Counter('bcc') + >>> c + Counter({'b': 4, 'c': 2, 'a': 1}) + + ''' + for elem, count in other.items(): + self[elem] += count + return self._keep_positive() + + def __isub__(self, other): + '''Inplace subtract counter, but keep only results with positive counts. + + >>> c = Counter('abbbc') + >>> c -= Counter('bccd') + >>> c + Counter({'b': 2, 'a': 1}) + + ''' + for elem, count in other.items(): + self[elem] -= count + return self._keep_positive() + + def __ior__(self, other): + '''Inplace union is the maximum of value from either counter. + + >>> c = Counter('abbb') + >>> c |= Counter('bcc') + >>> c + Counter({'b': 3, 'c': 2, 'a': 1}) + + ''' + for elem, other_count in other.items(): + count = self[elem] + if other_count > count: + self[elem] = other_count + return self._keep_positive() + + def __iand__(self, other): + '''Inplace intersection is the minimum of corresponding counts. + + >>> c = Counter('abbb') + >>> c &= Counter('bcc') + >>> c + Counter({'b': 1}) + + ''' + for elem, count in self.items(): + other_count = other[elem] + if other_count < count: + self[elem] = other_count + return self._keep_positive() + + +######################################################################## +### ChainMap +######################################################################## + +class ChainMap(_collections_abc.MutableMapping): + ''' A ChainMap groups multiple dicts (or other mappings) together + to create a single, updateable view. + + The underlying mappings are stored in a list. That list is public and can + be accessed or updated using the *maps* attribute. There is no other + state. + + Lookups search the underlying mappings successively until a key is found. + In contrast, writes, updates, and deletions only operate on the first + mapping. + + ''' + + def __init__(self, *maps): + '''Initialize a ChainMap by setting *maps* to the given mappings. + If no mappings are provided, a single empty dictionary is used. + + ''' + self.maps = list(maps) or [{}] # always at least one map + + def __missing__(self, key): + raise KeyError(key) + + def __getitem__(self, key): + for mapping in self.maps: + try: + return mapping[key] # can't use 'key in mapping' with defaultdict + except KeyError: + pass + return self.__missing__(key) # support subclasses that define __missing__ + + def get(self, key, default=None): + return self[key] if key in self else default + + def __len__(self): + return len(set().union(*self.maps)) # reuses stored hash values if possible + + def __iter__(self): + d = {} + for mapping in reversed(self.maps): + d.update(dict.fromkeys(mapping)) # reuses stored hash values if possible + return iter(d) + + def __contains__(self, key): + return any(key in m for m in self.maps) + + def __bool__(self): + return any(self.maps) + + @_recursive_repr() + def __repr__(self): + return f'{self.__class__.__name__}({", ".join(map(repr, self.maps))})' + + @classmethod + def fromkeys(cls, iterable, *args): + 'Create a ChainMap with a single dict created from the iterable.' + return cls(dict.fromkeys(iterable, *args)) + + def copy(self): + 'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]' + return self.__class__(self.maps[0].copy(), *self.maps[1:]) + + __copy__ = copy + + def new_child(self, m=None, **kwargs): # like Django's Context.push() + '''New ChainMap with a new map followed by all previous maps. + If no map is provided, an empty dict is used. + Keyword arguments update the map or new empty dict. + ''' + if m is None: + m = kwargs + elif kwargs: + m.update(kwargs) + return self.__class__(m, *self.maps) + + @property + def parents(self): # like Django's Context.pop() + 'New ChainMap from maps[1:].' + return self.__class__(*self.maps[1:]) + + def __setitem__(self, key, value): + self.maps[0][key] = value + + def __delitem__(self, key): + try: + del self.maps[0][key] + except KeyError: + raise KeyError(f'Key not found in the first mapping: {key!r}') + + def popitem(self): + 'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.' + try: + return self.maps[0].popitem() + except KeyError: + raise KeyError('No keys found in the first mapping.') + + def pop(self, key, *args): + 'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].' + try: + return self.maps[0].pop(key, *args) + except KeyError: + raise KeyError(f'Key not found in the first mapping: {key!r}') + + def clear(self): + 'Clear maps[0], leaving maps[1:] intact.' + self.maps[0].clear() + + def __ior__(self, other): + self.maps[0].update(other) + return self + + def __or__(self, other): + if not isinstance(other, _collections_abc.Mapping): + return NotImplemented + m = self.copy() + m.maps[0].update(other) + return m + + def __ror__(self, other): + if not isinstance(other, _collections_abc.Mapping): + return NotImplemented + m = dict(other) + for child in reversed(self.maps): + m.update(child) + return self.__class__(m) + + +################################################################################ +### UserDict +################################################################################ + +class UserDict(_collections_abc.MutableMapping): + + # Start by filling-out the abstract methods + def __init__(self, dict=None, /, **kwargs): + self.data = {} + if dict is not None: + self.update(dict) + if kwargs: + self.update(kwargs) + + def __len__(self): + return len(self.data) + + def __getitem__(self, key): + if key in self.data: + return self.data[key] + if hasattr(self.__class__, "__missing__"): + return self.__class__.__missing__(self, key) + raise KeyError(key) + + def __setitem__(self, key, item): + self.data[key] = item + + def __delitem__(self, key): + del self.data[key] + + def __iter__(self): + return iter(self.data) + + # Modify __contains__ to work correctly when __missing__ is present + def __contains__(self, key): + return key in self.data + + # Now, add the methods in dicts but not in MutableMapping + def __repr__(self): + return repr(self.data) + + def __or__(self, other): + if isinstance(other, UserDict): + return self.__class__(self.data | other.data) + if isinstance(other, dict): + return self.__class__(self.data | other) + return NotImplemented + + def __ror__(self, other): + if isinstance(other, UserDict): + return self.__class__(other.data | self.data) + if isinstance(other, dict): + return self.__class__(other | self.data) + return NotImplemented + + def __ior__(self, other): + if isinstance(other, UserDict): + self.data |= other.data + else: + self.data |= other + return self + + def __copy__(self): + inst = self.__class__.__new__(self.__class__) + inst.__dict__.update(self.__dict__) + # Create a copy and avoid triggering descriptors + inst.__dict__["data"] = self.__dict__["data"].copy() + return inst + + def copy(self): + if self.__class__ is UserDict: + return UserDict(self.data.copy()) + import copy + data = self.data + try: + self.data = {} + c = copy.copy(self) + finally: + self.data = data + c.update(self) + return c + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + +################################################################################ +### UserList +################################################################################ + +class UserList(_collections_abc.MutableSequence): + """A more or less complete user-defined wrapper around list objects.""" + + def __init__(self, initlist=None): + self.data = [] + if initlist is not None: + # XXX should this accept an arbitrary sequence? + if type(initlist) == type(self.data): + self.data[:] = initlist + elif isinstance(initlist, UserList): + self.data[:] = initlist.data[:] + else: + self.data = list(initlist) + + def __repr__(self): + return repr(self.data) + + def __lt__(self, other): + return self.data < self.__cast(other) + + def __le__(self, other): + return self.data <= self.__cast(other) + + def __eq__(self, other): + return self.data == self.__cast(other) + + def __gt__(self, other): + return self.data > self.__cast(other) + + def __ge__(self, other): + return self.data >= self.__cast(other) + + def __cast(self, other): + return other.data if isinstance(other, UserList) else other + + def __contains__(self, item): + return item in self.data + + def __len__(self): + return len(self.data) + + def __getitem__(self, i): + if isinstance(i, slice): + return self.__class__(self.data[i]) + else: + return self.data[i] + + def __setitem__(self, i, item): + self.data[i] = item + + def __delitem__(self, i): + del self.data[i] + + def __add__(self, other): + if isinstance(other, UserList): + return self.__class__(self.data + other.data) + elif isinstance(other, type(self.data)): + return self.__class__(self.data + other) + return self.__class__(self.data + list(other)) + + def __radd__(self, other): + if isinstance(other, UserList): + return self.__class__(other.data + self.data) + elif isinstance(other, type(self.data)): + return self.__class__(other + self.data) + return self.__class__(list(other) + self.data) + + def __iadd__(self, other): + if isinstance(other, UserList): + self.data += other.data + elif isinstance(other, type(self.data)): + self.data += other + else: + self.data += list(other) + return self + + def __mul__(self, n): + return self.__class__(self.data * n) + + __rmul__ = __mul__ + + def __imul__(self, n): + self.data *= n + return self + + def __copy__(self): + inst = self.__class__.__new__(self.__class__) + inst.__dict__.update(self.__dict__) + # Create a copy and avoid triggering descriptors + inst.__dict__["data"] = self.__dict__["data"][:] + return inst + + def append(self, item): + self.data.append(item) + + def insert(self, i, item): + self.data.insert(i, item) + + def pop(self, i=-1): + return self.data.pop(i) + + def remove(self, item): + self.data.remove(item) + + def clear(self): + self.data.clear() + + def copy(self): + return self.__class__(self) + + def count(self, item): + return self.data.count(item) + + def index(self, item, *args): + return self.data.index(item, *args) + + def reverse(self): + self.data.reverse() + + def sort(self, /, *args, **kwds): + self.data.sort(*args, **kwds) + + def extend(self, other): + if isinstance(other, UserList): + self.data.extend(other.data) + else: + self.data.extend(other) + + +################################################################################ +### UserString +################################################################################ + +class UserString(_collections_abc.Sequence): + + def __init__(self, seq): + if isinstance(seq, str): + self.data = seq + elif isinstance(seq, UserString): + self.data = seq.data[:] + else: + self.data = str(seq) + + def __str__(self): + return str(self.data) + + def __repr__(self): + return repr(self.data) + + def __int__(self): + return int(self.data) + + def __float__(self): + return float(self.data) + + def __complex__(self): + return complex(self.data) + + def __hash__(self): + return hash(self.data) + + def __getnewargs__(self): + return (self.data[:],) + + def __eq__(self, string): + if isinstance(string, UserString): + return self.data == string.data + return self.data == string + + def __lt__(self, string): + if isinstance(string, UserString): + return self.data < string.data + return self.data < string + + def __le__(self, string): + if isinstance(string, UserString): + return self.data <= string.data + return self.data <= string + + def __gt__(self, string): + if isinstance(string, UserString): + return self.data > string.data + return self.data > string + + def __ge__(self, string): + if isinstance(string, UserString): + return self.data >= string.data + return self.data >= string + + def __contains__(self, char): + if isinstance(char, UserString): + char = char.data + return char in self.data + + def __len__(self): + return len(self.data) + + def __getitem__(self, index): + return self.__class__(self.data[index]) + + def __add__(self, other): + if isinstance(other, UserString): + return self.__class__(self.data + other.data) + elif isinstance(other, str): + return self.__class__(self.data + other) + return self.__class__(self.data + str(other)) + + def __radd__(self, other): + if isinstance(other, str): + return self.__class__(other + self.data) + return self.__class__(str(other) + self.data) + + def __mul__(self, n): + return self.__class__(self.data * n) + + __rmul__ = __mul__ + + def __mod__(self, args): + return self.__class__(self.data % args) + + def __rmod__(self, template): + return self.__class__(str(template) % self) + + # the following methods are defined in alphabetical order: + def capitalize(self): + return self.__class__(self.data.capitalize()) + + def casefold(self): + return self.__class__(self.data.casefold()) + + def center(self, width, *args): + return self.__class__(self.data.center(width, *args)) + + def count(self, sub, start=0, end=_sys.maxsize): + if isinstance(sub, UserString): + sub = sub.data + return self.data.count(sub, start, end) + + def removeprefix(self, prefix, /): + if isinstance(prefix, UserString): + prefix = prefix.data + return self.__class__(self.data.removeprefix(prefix)) + + def removesuffix(self, suffix, /): + if isinstance(suffix, UserString): + suffix = suffix.data + return self.__class__(self.data.removesuffix(suffix)) + + def encode(self, encoding='utf-8', errors='strict'): + encoding = 'utf-8' if encoding is None else encoding + errors = 'strict' if errors is None else errors + return self.data.encode(encoding, errors) + + def endswith(self, suffix, start=0, end=_sys.maxsize): + return self.data.endswith(suffix, start, end) + + def expandtabs(self, tabsize=8): + return self.__class__(self.data.expandtabs(tabsize)) + + def find(self, sub, start=0, end=_sys.maxsize): + if isinstance(sub, UserString): + sub = sub.data + return self.data.find(sub, start, end) + + def format(self, /, *args, **kwds): + return self.data.format(*args, **kwds) + + def format_map(self, mapping): + return self.data.format_map(mapping) + + def index(self, sub, start=0, end=_sys.maxsize): + return self.data.index(sub, start, end) + + def isalpha(self): + return self.data.isalpha() + + def isalnum(self): + return self.data.isalnum() + + def isascii(self): + return self.data.isascii() + + def isdecimal(self): + return self.data.isdecimal() + + def isdigit(self): + return self.data.isdigit() + + def isidentifier(self): + return self.data.isidentifier() + + def islower(self): + return self.data.islower() + + def isnumeric(self): + return self.data.isnumeric() + + def isprintable(self): + return self.data.isprintable() + + def isspace(self): + return self.data.isspace() + + def istitle(self): + return self.data.istitle() + + def isupper(self): + return self.data.isupper() + + def join(self, seq): + return self.data.join(seq) + + def ljust(self, width, *args): + return self.__class__(self.data.ljust(width, *args)) + + def lower(self): + return self.__class__(self.data.lower()) + + def lstrip(self, chars=None): + return self.__class__(self.data.lstrip(chars)) + + maketrans = str.maketrans + + def partition(self, sep): + return self.data.partition(sep) + + def replace(self, old, new, maxsplit=-1): + if isinstance(old, UserString): + old = old.data + if isinstance(new, UserString): + new = new.data + return self.__class__(self.data.replace(old, new, maxsplit)) + + def rfind(self, sub, start=0, end=_sys.maxsize): + if isinstance(sub, UserString): + sub = sub.data + return self.data.rfind(sub, start, end) + + def rindex(self, sub, start=0, end=_sys.maxsize): + return self.data.rindex(sub, start, end) + + def rjust(self, width, *args): + return self.__class__(self.data.rjust(width, *args)) + + def rpartition(self, sep): + return self.data.rpartition(sep) + + def rstrip(self, chars=None): + return self.__class__(self.data.rstrip(chars)) + + def split(self, sep=None, maxsplit=-1): + return self.data.split(sep, maxsplit) + + def rsplit(self, sep=None, maxsplit=-1): + return self.data.rsplit(sep, maxsplit) + + def splitlines(self, keepends=False): + return self.data.splitlines(keepends) + + def startswith(self, prefix, start=0, end=_sys.maxsize): + return self.data.startswith(prefix, start, end) + + def strip(self, chars=None): + return self.__class__(self.data.strip(chars)) + + def swapcase(self): + return self.__class__(self.data.swapcase()) + + def title(self): + return self.__class__(self.data.title()) + + def translate(self, *args): + return self.__class__(self.data.translate(*args)) + + def upper(self): + return self.__class__(self.data.upper()) + + def zfill(self, width): + return self.__class__(self.data.zfill(width)) \ No newline at end of file From 9c2311cc618d6e29451bcd5f5d14737bb4aa5896 Mon Sep 17 00:00:00 2001 From: Petar Date: Thu, 20 Jul 2023 16:15:57 +0200 Subject: [PATCH 174/213] alpine image 3.15 --- images/ckan/2.9/Dockerfile | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 33ae654..00fea9b 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.16 as ckanbuild +FROM alpine:3.15 as ckanbuild # Used by Github Actions to tag the image with ENV IMAGE_TAG=2.9.9 @@ -70,7 +70,7 @@ RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.12.0 greenlet==1.1.3 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.16 as extbuild +FROM alpine:3.15 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -105,7 +105,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.16 +FROM alpine:3.15 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan @@ -154,8 +154,6 @@ RUN addgroup -g 92 -S ckan && \ adduser -u 92 -h /srv/app -H -D -S -G ckan ckan WORKDIR ${CKAN_DIR} -RUN ls -lah /usr/lib/python3.10/collections/ && cat /usr/lib/python3.10/collections/__init__.py -COPY __init__.py /usr/lib/python3.10/collections/__init__.py # Install CKAN RUN pip install -e /srv/app/src/ckan && \ From f5c546bab3ea015cc070e4ab29bc5edc2a9ac75b Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Fri, 21 Jul 2023 11:53:49 +0200 Subject: [PATCH 175/213] deleted old file --- images/ckan/2.9/__init__.py | 1560 ----------------------------------- 1 file changed, 1560 deletions(-) delete mode 100644 images/ckan/2.9/__init__.py diff --git a/images/ckan/2.9/__init__.py b/images/ckan/2.9/__init__.py deleted file mode 100644 index c418f07..0000000 --- a/images/ckan/2.9/__init__.py +++ /dev/null @@ -1,1560 +0,0 @@ -'''This module implements specialized container datatypes providing -alternatives to Python's general purpose built-in containers, dict, -list, set, and tuple. - -* namedtuple factory function for creating tuple subclasses with named fields -* deque list-like container with fast appends and pops on either end -* ChainMap dict-like class for creating a single view of multiple mappings -* Counter dict subclass for counting hashable objects -* OrderedDict dict subclass that remembers the order entries were added -* defaultdict dict subclass that calls a factory function to supply missing values -* UserDict wrapper around dictionary objects for easier dict subclassing -* UserList wrapper around list objects for easier list subclassing -* UserString wrapper around string objects for easier string subclassing - -''' - -__all__ = [ - 'ChainMap', - 'Counter', - 'OrderedDict', - 'UserDict', - 'UserList', - 'UserString', - 'defaultdict', - 'deque', - 'namedtuple', -] - -from _collections_abc import Mapping -from _collections_abc import MutableMapping -from _collections_abc import Sequence - -import _collections_abc -import sys as _sys - -from itertools import chain as _chain -from itertools import repeat as _repeat -from itertools import starmap as _starmap -from keyword import iskeyword as _iskeyword -from operator import eq as _eq -from operator import itemgetter as _itemgetter -from reprlib import recursive_repr as _recursive_repr -from _weakref import proxy as _proxy - -try: - from _collections import deque -except ImportError: - pass -else: - _collections_abc.MutableSequence.register(deque) - -try: - from _collections import defaultdict -except ImportError: - pass - - -################################################################################ -### OrderedDict -################################################################################ - -class _OrderedDictKeysView(_collections_abc.KeysView): - - def __reversed__(self): - yield from reversed(self._mapping) - -class _OrderedDictItemsView(_collections_abc.ItemsView): - - def __reversed__(self): - for key in reversed(self._mapping): - yield (key, self._mapping[key]) - -class _OrderedDictValuesView(_collections_abc.ValuesView): - - def __reversed__(self): - for key in reversed(self._mapping): - yield self._mapping[key] - -class _Link(object): - __slots__ = 'prev', 'next', 'key', '__weakref__' - -class OrderedDict(dict): - 'Dictionary that remembers insertion order' - # An inherited dict maps keys to values. - # The inherited dict provides __getitem__, __len__, __contains__, and get. - # The remaining methods are order-aware. - # Big-O running times for all methods are the same as regular dictionaries. - - # The internal self.__map dict maps keys to links in a doubly linked list. - # The circular doubly linked list starts and ends with a sentinel element. - # The sentinel element never gets deleted (this simplifies the algorithm). - # The sentinel is in self.__hardroot with a weakref proxy in self.__root. - # The prev links are weakref proxies (to prevent circular references). - # Individual links are kept alive by the hard reference in self.__map. - # Those hard references disappear when a key is deleted from an OrderedDict. - - def __init__(self, other=(), /, **kwds): - '''Initialize an ordered dictionary. The signature is the same as - regular dictionaries. Keyword argument order is preserved. - ''' - try: - self.__root - except AttributeError: - self.__hardroot = _Link() - self.__root = root = _proxy(self.__hardroot) - root.prev = root.next = root - self.__map = {} - self.__update(other, **kwds) - - def __setitem__(self, key, value, - dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link): - 'od.__setitem__(i, y) <==> od[i]=y' - # Setting a new item creates a new link at the end of the linked list, - # and the inherited dictionary is updated with the new key/value pair. - if key not in self: - self.__map[key] = link = Link() - root = self.__root - last = root.prev - link.prev, link.next, link.key = last, root, key - last.next = link - root.prev = proxy(link) - dict_setitem(self, key, value) - - def __delitem__(self, key, dict_delitem=dict.__delitem__): - 'od.__delitem__(y) <==> del od[y]' - # Deleting an existing item uses self.__map to find the link which gets - # removed by updating the links in the predecessor and successor nodes. - dict_delitem(self, key) - link = self.__map.pop(key) - link_prev = link.prev - link_next = link.next - link_prev.next = link_next - link_next.prev = link_prev - link.prev = None - link.next = None - - def __iter__(self): - 'od.__iter__() <==> iter(od)' - # Traverse the linked list in order. - root = self.__root - curr = root.next - while curr is not root: - yield curr.key - curr = curr.next - - def __reversed__(self): - 'od.__reversed__() <==> reversed(od)' - # Traverse the linked list in reverse order. - root = self.__root - curr = root.prev - while curr is not root: - yield curr.key - curr = curr.prev - - def clear(self): - 'od.clear() -> None. Remove all items from od.' - root = self.__root - root.prev = root.next = root - self.__map.clear() - dict.clear(self) - - def popitem(self, last=True): - '''Remove and return a (key, value) pair from the dictionary. - - Pairs are returned in LIFO order if last is true or FIFO order if false. - ''' - if not self: - raise KeyError('dictionary is empty') - root = self.__root - if last: - link = root.prev - link_prev = link.prev - link_prev.next = root - root.prev = link_prev - else: - link = root.next - link_next = link.next - root.next = link_next - link_next.prev = root - key = link.key - del self.__map[key] - value = dict.pop(self, key) - return key, value - - def move_to_end(self, key, last=True): - '''Move an existing element to the end (or beginning if last is false). - - Raise KeyError if the element does not exist. - ''' - link = self.__map[key] - link_prev = link.prev - link_next = link.next - soft_link = link_next.prev - link_prev.next = link_next - link_next.prev = link_prev - root = self.__root - if last: - last = root.prev - link.prev = last - link.next = root - root.prev = soft_link - last.next = link - else: - first = root.next - link.prev = root - link.next = first - first.prev = soft_link - root.next = link - - def __sizeof__(self): - sizeof = _sys.getsizeof - n = len(self) + 1 # number of links including root - size = sizeof(self.__dict__) # instance dictionary - size += sizeof(self.__map) * 2 # internal dict and inherited dict - size += sizeof(self.__hardroot) * n # link objects - size += sizeof(self.__root) * n # proxy objects - return size - - update = __update = _collections_abc.MutableMapping.update - - def keys(self): - "D.keys() -> a set-like object providing a view on D's keys" - return _OrderedDictKeysView(self) - - def items(self): - "D.items() -> a set-like object providing a view on D's items" - return _OrderedDictItemsView(self) - - def values(self): - "D.values() -> an object providing a view on D's values" - return _OrderedDictValuesView(self) - - __ne__ = _collections_abc.MutableMapping.__ne__ - - __marker = object() - - def pop(self, key, default=__marker): - '''od.pop(k[,d]) -> v, remove specified key and return the corresponding - value. If key is not found, d is returned if given, otherwise KeyError - is raised. - - ''' - if key in self: - result = self[key] - del self[key] - return result - if default is self.__marker: - raise KeyError(key) - return default - - def setdefault(self, key, default=None): - '''Insert key with a value of default if key is not in the dictionary. - - Return the value for key if key is in the dictionary, else default. - ''' - if key in self: - return self[key] - self[key] = default - return default - - @_recursive_repr() - def __repr__(self): - 'od.__repr__() <==> repr(od)' - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, list(self.items())) - - def __reduce__(self): - 'Return state information for pickling' - inst_dict = vars(self).copy() - for k in vars(OrderedDict()): - inst_dict.pop(k, None) - return self.__class__, (), inst_dict or None, None, iter(self.items()) - - def copy(self): - 'od.copy() -> a shallow copy of od' - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - '''Create a new ordered dictionary with keys from iterable and values set to value. - ''' - self = cls() - for key in iterable: - self[key] = value - return self - - def __eq__(self, other): - '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive - while comparison to a regular mapping is order-insensitive. - - ''' - if isinstance(other, OrderedDict): - return dict.__eq__(self, other) and all(map(_eq, self, other)) - return dict.__eq__(self, other) - - def __ior__(self, other): - self.update(other) - return self - - def __or__(self, other): - if not isinstance(other, dict): - return NotImplemented - new = self.__class__(self) - new.update(other) - return new - - def __ror__(self, other): - if not isinstance(other, dict): - return NotImplemented - new = self.__class__(other) - new.update(self) - return new - - -try: - from _collections import OrderedDict -except ImportError: - # Leave the pure Python version in place. - pass - - -################################################################################ -### namedtuple -################################################################################ - -try: - from _collections import _tuplegetter -except ImportError: - _tuplegetter = lambda index, doc: property(_itemgetter(index), doc=doc) - -def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None): - """Returns a new subclass of tuple with named fields. - - >>> Point = namedtuple('Point', ['x', 'y']) - >>> Point.__doc__ # docstring for the new class - 'Point(x, y)' - >>> p = Point(11, y=22) # instantiate with positional args or keywords - >>> p[0] + p[1] # indexable like a plain tuple - 33 - >>> x, y = p # unpack like a regular tuple - >>> x, y - (11, 22) - >>> p.x + p.y # fields also accessible by name - 33 - >>> d = p._asdict() # convert to a dictionary - >>> d['x'] - 11 - >>> Point(**d) # convert from a dictionary - Point(x=11, y=22) - >>> p._replace(x=100) # _replace() is like str.replace() but targets named fields - Point(x=100, y=22) - - """ - - # Validate the field names. At the user's option, either generate an error - # message or automatically replace the field name with a valid name. - if isinstance(field_names, str): - field_names = field_names.replace(',', ' ').split() - field_names = list(map(str, field_names)) - typename = _sys.intern(str(typename)) - - if rename: - seen = set() - for index, name in enumerate(field_names): - if (not name.isidentifier() - or _iskeyword(name) - or name.startswith('_') - or name in seen): - field_names[index] = f'_{index}' - seen.add(name) - - for name in [typename] + field_names: - if type(name) is not str: - raise TypeError('Type names and field names must be strings') - if not name.isidentifier(): - raise ValueError('Type names and field names must be valid ' - f'identifiers: {name!r}') - if _iskeyword(name): - raise ValueError('Type names and field names cannot be a ' - f'keyword: {name!r}') - - seen = set() - for name in field_names: - if name.startswith('_') and not rename: - raise ValueError('Field names cannot start with an underscore: ' - f'{name!r}') - if name in seen: - raise ValueError(f'Encountered duplicate field name: {name!r}') - seen.add(name) - - field_defaults = {} - if defaults is not None: - defaults = tuple(defaults) - if len(defaults) > len(field_names): - raise TypeError('Got more default values than field names') - field_defaults = dict(reversed(list(zip(reversed(field_names), - reversed(defaults))))) - - # Variables used in the methods and docstrings - field_names = tuple(map(_sys.intern, field_names)) - num_fields = len(field_names) - arg_list = ', '.join(field_names) - if num_fields == 1: - arg_list += ',' - repr_fmt = '(' + ', '.join(f'{name}=%r' for name in field_names) + ')' - tuple_new = tuple.__new__ - _dict, _tuple, _len, _map, _zip = dict, tuple, len, map, zip - - # Create all the named tuple methods to be added to the class namespace - - namespace = { - '_tuple_new': tuple_new, - '__builtins__': {}, - '__name__': f'namedtuple_{typename}', - } - code = f'lambda _cls, {arg_list}: _tuple_new(_cls, ({arg_list}))' - __new__ = eval(code, namespace) - __new__.__name__ = '__new__' - __new__.__doc__ = f'Create new instance of {typename}({arg_list})' - if defaults is not None: - __new__.__defaults__ = defaults - - @classmethod - def _make(cls, iterable): - result = tuple_new(cls, iterable) - if _len(result) != num_fields: - raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') - return result - - _make.__func__.__doc__ = (f'Make a new {typename} object from a sequence ' - 'or iterable') - - def _replace(self, /, **kwds): - result = self._make(_map(kwds.pop, field_names, self)) - if kwds: - raise ValueError(f'Got unexpected field names: {list(kwds)!r}') - return result - - _replace.__doc__ = (f'Return a new {typename} object replacing specified ' - 'fields with new values') - - def __repr__(self): - 'Return a nicely formatted representation string' - return self.__class__.__name__ + repr_fmt % self - - def _asdict(self): - 'Return a new dict which maps field names to their values.' - return _dict(_zip(self._fields, self)) - - def __getnewargs__(self): - 'Return self as a plain tuple. Used by copy and pickle.' - return _tuple(self) - - # Modify function metadata to help with introspection and debugging - for method in ( - __new__, - _make.__func__, - _replace, - __repr__, - _asdict, - __getnewargs__, - ): - method.__qualname__ = f'{typename}.{method.__name__}' - - # Build-up the class namespace dictionary - # and use type() to build the result class - class_namespace = { - '__doc__': f'{typename}({arg_list})', - '__slots__': (), - '_fields': field_names, - '_field_defaults': field_defaults, - '__new__': __new__, - '_make': _make, - '_replace': _replace, - '__repr__': __repr__, - '_asdict': _asdict, - '__getnewargs__': __getnewargs__, - '__match_args__': field_names, - } - for index, name in enumerate(field_names): - doc = _sys.intern(f'Alias for field number {index}') - class_namespace[name] = _tuplegetter(index, doc) - - result = type(typename, (tuple,), class_namespace) - - # For pickling to work, the __module__ variable needs to be set to the frame - # where the named tuple is created. Bypass this step in environments where - # sys._getframe is not defined (Jython for example) or sys._getframe is not - # defined for arguments greater than 0 (IronPython), or where the user has - # specified a particular module. - if module is None: - try: - module = _sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - pass - if module is not None: - result.__module__ = module - - return result - - -######################################################################## -### Counter -######################################################################## - -def _count_elements(mapping, iterable): - 'Tally elements from the iterable.' - mapping_get = mapping.get - for elem in iterable: - mapping[elem] = mapping_get(elem, 0) + 1 - -try: # Load C helper function if available - from _collections import _count_elements -except ImportError: - pass - -class Counter(dict): - '''Dict subclass for counting hashable items. Sometimes called a bag - or multiset. Elements are stored as dictionary keys and their counts - are stored as dictionary values. - - >>> c = Counter('abcdeabcdabcaba') # count elements from a string - - >>> c.most_common(3) # three most common elements - [('a', 5), ('b', 4), ('c', 3)] - >>> sorted(c) # list all unique elements - ['a', 'b', 'c', 'd', 'e'] - >>> ''.join(sorted(c.elements())) # list elements with repetitions - 'aaaaabbbbcccdde' - >>> sum(c.values()) # total of all counts - 15 - - >>> c['a'] # count of letter 'a' - 5 - >>> for elem in 'shazam': # update counts from an iterable - ... c[elem] += 1 # by adding 1 to each element's count - >>> c['a'] # now there are seven 'a' - 7 - >>> del c['b'] # remove all 'b' - >>> c['b'] # now there are zero 'b' - 0 - - >>> d = Counter('simsalabim') # make another counter - >>> c.update(d) # add in the second counter - >>> c['a'] # now there are nine 'a' - 9 - - >>> c.clear() # empty the counter - >>> c - Counter() - - Note: If a count is set to zero or reduced to zero, it will remain - in the counter until the entry is deleted or the counter is cleared: - - >>> c = Counter('aaabbc') - >>> c['b'] -= 2 # reduce the count of 'b' by two - >>> c.most_common() # 'b' is still in, but its count is zero - [('a', 3), ('c', 1), ('b', 0)] - - ''' - # References: - # http://en.wikipedia.org/wiki/Multiset - # http://www.gnu.org/software/smalltalk/manual-base/html_node/Bag.html - # http://www.demo2s.com/Tutorial/Cpp/0380__set-multiset/Catalog0380__set-multiset.htm - # http://code.activestate.com/recipes/259174/ - # Knuth, TAOCP Vol. II section 4.6.3 - - def __init__(self, iterable=None, /, **kwds): - '''Create a new, empty Counter object. And if given, count elements - from an input iterable. Or, initialize the count from another mapping - of elements to their counts. - - >>> c = Counter() # a new, empty counter - >>> c = Counter('gallahad') # a new counter from an iterable - >>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping - >>> c = Counter(a=4, b=2) # a new counter from keyword args - - ''' - super().__init__() - self.update(iterable, **kwds) - - def __missing__(self, key): - 'The count of elements not in the Counter is zero.' - # Needed so that self[missing_item] does not raise KeyError - return 0 - - def total(self): - 'Sum of the counts' - return sum(self.values()) - - def most_common(self, n=None): - '''List the n most common elements and their counts from the most - common to the least. If n is None, then list all element counts. - - >>> Counter('abracadabra').most_common(3) - [('a', 5), ('b', 2), ('r', 2)] - - ''' - # Emulate Bag.sortedByCount from Smalltalk - if n is None: - return sorted(self.items(), key=_itemgetter(1), reverse=True) - - # Lazy import to speedup Python startup time - import heapq - return heapq.nlargest(n, self.items(), key=_itemgetter(1)) - - def elements(self): - '''Iterator over elements repeating each as many times as its count. - - >>> c = Counter('ABCABC') - >>> sorted(c.elements()) - ['A', 'A', 'B', 'B', 'C', 'C'] - - # Knuth's example for prime factors of 1836: 2**2 * 3**3 * 17**1 - >>> prime_factors = Counter({2: 2, 3: 3, 17: 1}) - >>> product = 1 - >>> for factor in prime_factors.elements(): # loop over factors - ... product *= factor # and multiply them - >>> product - 1836 - - Note, if an element's count has been set to zero or is a negative - number, elements() will ignore it. - - ''' - # Emulate Bag.do from Smalltalk and Multiset.begin from C++. - return _chain.from_iterable(_starmap(_repeat, self.items())) - - # Override dict methods where necessary - - @classmethod - def fromkeys(cls, iterable, v=None): - # There is no equivalent method for counters because the semantics - # would be ambiguous in cases such as Counter.fromkeys('aaabbc', v=2). - # Initializing counters to zero values isn't necessary because zero - # is already the default value for counter lookups. Initializing - # to one is easily accomplished with Counter(set(iterable)). For - # more exotic cases, create a dictionary first using a dictionary - # comprehension or dict.fromkeys(). - raise NotImplementedError( - 'Counter.fromkeys() is undefined. Use Counter(iterable) instead.') - - def update(self, iterable=None, /, **kwds): - '''Like dict.update() but add counts instead of replacing them. - - Source can be an iterable, a dictionary, or another Counter instance. - - >>> c = Counter('which') - >>> c.update('witch') # add elements from another iterable - >>> d = Counter('watch') - >>> c.update(d) # add elements from another counter - >>> c['h'] # four 'h' in which, witch, and watch - 4 - - ''' - # The regular dict.update() operation makes no sense here because the - # replace behavior results in the some of original untouched counts - # being mixed-in with all of the other counts for a mismash that - # doesn't have a straight-forward interpretation in most counting - # contexts. Instead, we implement straight-addition. Both the inputs - # and outputs are allowed to contain zero and negative counts. - - if iterable is not None: - if isinstance(iterable, _collections_abc.Mapping): - if self: - self_get = self.get - for elem, count in iterable.items(): - self[elem] = count + self_get(elem, 0) - else: - # fast path when counter is empty - super().update(iterable) - else: - _count_elements(self, iterable) - if kwds: - self.update(kwds) - - def subtract(self, iterable=None, /, **kwds): - '''Like dict.update() but subtracts counts instead of replacing them. - Counts can be reduced below zero. Both the inputs and outputs are - allowed to contain zero and negative counts. - - Source can be an iterable, a dictionary, or another Counter instance. - - >>> c = Counter('which') - >>> c.subtract('witch') # subtract elements from another iterable - >>> c.subtract(Counter('watch')) # subtract elements from another counter - >>> c['h'] # 2 in which, minus 1 in witch, minus 1 in watch - 0 - >>> c['w'] # 1 in which, minus 1 in witch, minus 1 in watch - -1 - - ''' - if iterable is not None: - self_get = self.get - if isinstance(iterable, _collections_abc.Mapping): - for elem, count in iterable.items(): - self[elem] = self_get(elem, 0) - count - else: - for elem in iterable: - self[elem] = self_get(elem, 0) - 1 - if kwds: - self.subtract(kwds) - - def copy(self): - 'Return a shallow copy.' - return self.__class__(self) - - def __reduce__(self): - return self.__class__, (dict(self),) - - def __delitem__(self, elem): - 'Like dict.__delitem__() but does not raise KeyError for missing values.' - if elem in self: - super().__delitem__(elem) - - def __eq__(self, other): - 'True if all counts agree. Missing counts are treated as zero.' - if not isinstance(other, Counter): - return NotImplemented - return all(self[e] == other[e] for c in (self, other) for e in c) - - def __ne__(self, other): - 'True if any counts disagree. Missing counts are treated as zero.' - if not isinstance(other, Counter): - return NotImplemented - return not self == other - - def __le__(self, other): - 'True if all counts in self are a subset of those in other.' - if not isinstance(other, Counter): - return NotImplemented - return all(self[e] <= other[e] for c in (self, other) for e in c) - - def __lt__(self, other): - 'True if all counts in self are a proper subset of those in other.' - if not isinstance(other, Counter): - return NotImplemented - return self <= other and self != other - - def __ge__(self, other): - 'True if all counts in self are a superset of those in other.' - if not isinstance(other, Counter): - return NotImplemented - return all(self[e] >= other[e] for c in (self, other) for e in c) - - def __gt__(self, other): - 'True if all counts in self are a proper superset of those in other.' - if not isinstance(other, Counter): - return NotImplemented - return self >= other and self != other - - def __repr__(self): - if not self: - return f'{self.__class__.__name__}()' - try: - # dict() preserves the ordering returned by most_common() - d = dict(self.most_common()) - except TypeError: - # handle case where values are not orderable - d = dict(self) - return f'{self.__class__.__name__}({d!r})' - - # Multiset-style mathematical operations discussed in: - # Knuth TAOCP Volume II section 4.6.3 exercise 19 - # and at http://en.wikipedia.org/wiki/Multiset - # - # Outputs guaranteed to only include positive counts. - # - # To strip negative and zero counts, add-in an empty counter: - # c += Counter() - # - # Results are ordered according to when an element is first - # encountered in the left operand and then by the order - # encountered in the right operand. - # - # When the multiplicities are all zero or one, multiset operations - # are guaranteed to be equivalent to the corresponding operations - # for regular sets. - # Given counter multisets such as: - # cp = Counter(a=1, b=0, c=1) - # cq = Counter(c=1, d=0, e=1) - # The corresponding regular sets would be: - # sp = {'a', 'c'} - # sq = {'c', 'e'} - # All of the following relations would hold: - # set(cp + cq) == sp | sq - # set(cp - cq) == sp - sq - # set(cp | cq) == sp | sq - # set(cp & cq) == sp & sq - # (cp == cq) == (sp == sq) - # (cp != cq) == (sp != sq) - # (cp <= cq) == (sp <= sq) - # (cp < cq) == (sp < sq) - # (cp >= cq) == (sp >= sq) - # (cp > cq) == (sp > sq) - - def __add__(self, other): - '''Add counts from two counters. - - >>> Counter('abbb') + Counter('bcc') - Counter({'b': 4, 'c': 2, 'a': 1}) - - ''' - if not isinstance(other, Counter): - return NotImplemented - result = Counter() - for elem, count in self.items(): - newcount = count + other[elem] - if newcount > 0: - result[elem] = newcount - for elem, count in other.items(): - if elem not in self and count > 0: - result[elem] = count - return result - - def __sub__(self, other): - ''' Subtract count, but keep only results with positive counts. - - >>> Counter('abbbc') - Counter('bccd') - Counter({'b': 2, 'a': 1}) - - ''' - if not isinstance(other, Counter): - return NotImplemented - result = Counter() - for elem, count in self.items(): - newcount = count - other[elem] - if newcount > 0: - result[elem] = newcount - for elem, count in other.items(): - if elem not in self and count < 0: - result[elem] = 0 - count - return result - - def __or__(self, other): - '''Union is the maximum of value in either of the input counters. - - >>> Counter('abbb') | Counter('bcc') - Counter({'b': 3, 'c': 2, 'a': 1}) - - ''' - if not isinstance(other, Counter): - return NotImplemented - result = Counter() - for elem, count in self.items(): - other_count = other[elem] - newcount = other_count if count < other_count else count - if newcount > 0: - result[elem] = newcount - for elem, count in other.items(): - if elem not in self and count > 0: - result[elem] = count - return result - - def __and__(self, other): - ''' Intersection is the minimum of corresponding counts. - - >>> Counter('abbb') & Counter('bcc') - Counter({'b': 1}) - - ''' - if not isinstance(other, Counter): - return NotImplemented - result = Counter() - for elem, count in self.items(): - other_count = other[elem] - newcount = count if count < other_count else other_count - if newcount > 0: - result[elem] = newcount - return result - - def __pos__(self): - 'Adds an empty counter, effectively stripping negative and zero counts' - result = Counter() - for elem, count in self.items(): - if count > 0: - result[elem] = count - return result - - def __neg__(self): - '''Subtracts from an empty counter. Strips positive and zero counts, - and flips the sign on negative counts. - - ''' - result = Counter() - for elem, count in self.items(): - if count < 0: - result[elem] = 0 - count - return result - - def _keep_positive(self): - '''Internal method to strip elements with a negative or zero count''' - nonpositive = [elem for elem, count in self.items() if not count > 0] - for elem in nonpositive: - del self[elem] - return self - - def __iadd__(self, other): - '''Inplace add from another counter, keeping only positive counts. - - >>> c = Counter('abbb') - >>> c += Counter('bcc') - >>> c - Counter({'b': 4, 'c': 2, 'a': 1}) - - ''' - for elem, count in other.items(): - self[elem] += count - return self._keep_positive() - - def __isub__(self, other): - '''Inplace subtract counter, but keep only results with positive counts. - - >>> c = Counter('abbbc') - >>> c -= Counter('bccd') - >>> c - Counter({'b': 2, 'a': 1}) - - ''' - for elem, count in other.items(): - self[elem] -= count - return self._keep_positive() - - def __ior__(self, other): - '''Inplace union is the maximum of value from either counter. - - >>> c = Counter('abbb') - >>> c |= Counter('bcc') - >>> c - Counter({'b': 3, 'c': 2, 'a': 1}) - - ''' - for elem, other_count in other.items(): - count = self[elem] - if other_count > count: - self[elem] = other_count - return self._keep_positive() - - def __iand__(self, other): - '''Inplace intersection is the minimum of corresponding counts. - - >>> c = Counter('abbb') - >>> c &= Counter('bcc') - >>> c - Counter({'b': 1}) - - ''' - for elem, count in self.items(): - other_count = other[elem] - if other_count < count: - self[elem] = other_count - return self._keep_positive() - - -######################################################################## -### ChainMap -######################################################################## - -class ChainMap(_collections_abc.MutableMapping): - ''' A ChainMap groups multiple dicts (or other mappings) together - to create a single, updateable view. - - The underlying mappings are stored in a list. That list is public and can - be accessed or updated using the *maps* attribute. There is no other - state. - - Lookups search the underlying mappings successively until a key is found. - In contrast, writes, updates, and deletions only operate on the first - mapping. - - ''' - - def __init__(self, *maps): - '''Initialize a ChainMap by setting *maps* to the given mappings. - If no mappings are provided, a single empty dictionary is used. - - ''' - self.maps = list(maps) or [{}] # always at least one map - - def __missing__(self, key): - raise KeyError(key) - - def __getitem__(self, key): - for mapping in self.maps: - try: - return mapping[key] # can't use 'key in mapping' with defaultdict - except KeyError: - pass - return self.__missing__(key) # support subclasses that define __missing__ - - def get(self, key, default=None): - return self[key] if key in self else default - - def __len__(self): - return len(set().union(*self.maps)) # reuses stored hash values if possible - - def __iter__(self): - d = {} - for mapping in reversed(self.maps): - d.update(dict.fromkeys(mapping)) # reuses stored hash values if possible - return iter(d) - - def __contains__(self, key): - return any(key in m for m in self.maps) - - def __bool__(self): - return any(self.maps) - - @_recursive_repr() - def __repr__(self): - return f'{self.__class__.__name__}({", ".join(map(repr, self.maps))})' - - @classmethod - def fromkeys(cls, iterable, *args): - 'Create a ChainMap with a single dict created from the iterable.' - return cls(dict.fromkeys(iterable, *args)) - - def copy(self): - 'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]' - return self.__class__(self.maps[0].copy(), *self.maps[1:]) - - __copy__ = copy - - def new_child(self, m=None, **kwargs): # like Django's Context.push() - '''New ChainMap with a new map followed by all previous maps. - If no map is provided, an empty dict is used. - Keyword arguments update the map or new empty dict. - ''' - if m is None: - m = kwargs - elif kwargs: - m.update(kwargs) - return self.__class__(m, *self.maps) - - @property - def parents(self): # like Django's Context.pop() - 'New ChainMap from maps[1:].' - return self.__class__(*self.maps[1:]) - - def __setitem__(self, key, value): - self.maps[0][key] = value - - def __delitem__(self, key): - try: - del self.maps[0][key] - except KeyError: - raise KeyError(f'Key not found in the first mapping: {key!r}') - - def popitem(self): - 'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.' - try: - return self.maps[0].popitem() - except KeyError: - raise KeyError('No keys found in the first mapping.') - - def pop(self, key, *args): - 'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].' - try: - return self.maps[0].pop(key, *args) - except KeyError: - raise KeyError(f'Key not found in the first mapping: {key!r}') - - def clear(self): - 'Clear maps[0], leaving maps[1:] intact.' - self.maps[0].clear() - - def __ior__(self, other): - self.maps[0].update(other) - return self - - def __or__(self, other): - if not isinstance(other, _collections_abc.Mapping): - return NotImplemented - m = self.copy() - m.maps[0].update(other) - return m - - def __ror__(self, other): - if not isinstance(other, _collections_abc.Mapping): - return NotImplemented - m = dict(other) - for child in reversed(self.maps): - m.update(child) - return self.__class__(m) - - -################################################################################ -### UserDict -################################################################################ - -class UserDict(_collections_abc.MutableMapping): - - # Start by filling-out the abstract methods - def __init__(self, dict=None, /, **kwargs): - self.data = {} - if dict is not None: - self.update(dict) - if kwargs: - self.update(kwargs) - - def __len__(self): - return len(self.data) - - def __getitem__(self, key): - if key in self.data: - return self.data[key] - if hasattr(self.__class__, "__missing__"): - return self.__class__.__missing__(self, key) - raise KeyError(key) - - def __setitem__(self, key, item): - self.data[key] = item - - def __delitem__(self, key): - del self.data[key] - - def __iter__(self): - return iter(self.data) - - # Modify __contains__ to work correctly when __missing__ is present - def __contains__(self, key): - return key in self.data - - # Now, add the methods in dicts but not in MutableMapping - def __repr__(self): - return repr(self.data) - - def __or__(self, other): - if isinstance(other, UserDict): - return self.__class__(self.data | other.data) - if isinstance(other, dict): - return self.__class__(self.data | other) - return NotImplemented - - def __ror__(self, other): - if isinstance(other, UserDict): - return self.__class__(other.data | self.data) - if isinstance(other, dict): - return self.__class__(other | self.data) - return NotImplemented - - def __ior__(self, other): - if isinstance(other, UserDict): - self.data |= other.data - else: - self.data |= other - return self - - def __copy__(self): - inst = self.__class__.__new__(self.__class__) - inst.__dict__.update(self.__dict__) - # Create a copy and avoid triggering descriptors - inst.__dict__["data"] = self.__dict__["data"].copy() - return inst - - def copy(self): - if self.__class__ is UserDict: - return UserDict(self.data.copy()) - import copy - data = self.data - try: - self.data = {} - c = copy.copy(self) - finally: - self.data = data - c.update(self) - return c - - @classmethod - def fromkeys(cls, iterable, value=None): - d = cls() - for key in iterable: - d[key] = value - return d - - -################################################################################ -### UserList -################################################################################ - -class UserList(_collections_abc.MutableSequence): - """A more or less complete user-defined wrapper around list objects.""" - - def __init__(self, initlist=None): - self.data = [] - if initlist is not None: - # XXX should this accept an arbitrary sequence? - if type(initlist) == type(self.data): - self.data[:] = initlist - elif isinstance(initlist, UserList): - self.data[:] = initlist.data[:] - else: - self.data = list(initlist) - - def __repr__(self): - return repr(self.data) - - def __lt__(self, other): - return self.data < self.__cast(other) - - def __le__(self, other): - return self.data <= self.__cast(other) - - def __eq__(self, other): - return self.data == self.__cast(other) - - def __gt__(self, other): - return self.data > self.__cast(other) - - def __ge__(self, other): - return self.data >= self.__cast(other) - - def __cast(self, other): - return other.data if isinstance(other, UserList) else other - - def __contains__(self, item): - return item in self.data - - def __len__(self): - return len(self.data) - - def __getitem__(self, i): - if isinstance(i, slice): - return self.__class__(self.data[i]) - else: - return self.data[i] - - def __setitem__(self, i, item): - self.data[i] = item - - def __delitem__(self, i): - del self.data[i] - - def __add__(self, other): - if isinstance(other, UserList): - return self.__class__(self.data + other.data) - elif isinstance(other, type(self.data)): - return self.__class__(self.data + other) - return self.__class__(self.data + list(other)) - - def __radd__(self, other): - if isinstance(other, UserList): - return self.__class__(other.data + self.data) - elif isinstance(other, type(self.data)): - return self.__class__(other + self.data) - return self.__class__(list(other) + self.data) - - def __iadd__(self, other): - if isinstance(other, UserList): - self.data += other.data - elif isinstance(other, type(self.data)): - self.data += other - else: - self.data += list(other) - return self - - def __mul__(self, n): - return self.__class__(self.data * n) - - __rmul__ = __mul__ - - def __imul__(self, n): - self.data *= n - return self - - def __copy__(self): - inst = self.__class__.__new__(self.__class__) - inst.__dict__.update(self.__dict__) - # Create a copy and avoid triggering descriptors - inst.__dict__["data"] = self.__dict__["data"][:] - return inst - - def append(self, item): - self.data.append(item) - - def insert(self, i, item): - self.data.insert(i, item) - - def pop(self, i=-1): - return self.data.pop(i) - - def remove(self, item): - self.data.remove(item) - - def clear(self): - self.data.clear() - - def copy(self): - return self.__class__(self) - - def count(self, item): - return self.data.count(item) - - def index(self, item, *args): - return self.data.index(item, *args) - - def reverse(self): - self.data.reverse() - - def sort(self, /, *args, **kwds): - self.data.sort(*args, **kwds) - - def extend(self, other): - if isinstance(other, UserList): - self.data.extend(other.data) - else: - self.data.extend(other) - - -################################################################################ -### UserString -################################################################################ - -class UserString(_collections_abc.Sequence): - - def __init__(self, seq): - if isinstance(seq, str): - self.data = seq - elif isinstance(seq, UserString): - self.data = seq.data[:] - else: - self.data = str(seq) - - def __str__(self): - return str(self.data) - - def __repr__(self): - return repr(self.data) - - def __int__(self): - return int(self.data) - - def __float__(self): - return float(self.data) - - def __complex__(self): - return complex(self.data) - - def __hash__(self): - return hash(self.data) - - def __getnewargs__(self): - return (self.data[:],) - - def __eq__(self, string): - if isinstance(string, UserString): - return self.data == string.data - return self.data == string - - def __lt__(self, string): - if isinstance(string, UserString): - return self.data < string.data - return self.data < string - - def __le__(self, string): - if isinstance(string, UserString): - return self.data <= string.data - return self.data <= string - - def __gt__(self, string): - if isinstance(string, UserString): - return self.data > string.data - return self.data > string - - def __ge__(self, string): - if isinstance(string, UserString): - return self.data >= string.data - return self.data >= string - - def __contains__(self, char): - if isinstance(char, UserString): - char = char.data - return char in self.data - - def __len__(self): - return len(self.data) - - def __getitem__(self, index): - return self.__class__(self.data[index]) - - def __add__(self, other): - if isinstance(other, UserString): - return self.__class__(self.data + other.data) - elif isinstance(other, str): - return self.__class__(self.data + other) - return self.__class__(self.data + str(other)) - - def __radd__(self, other): - if isinstance(other, str): - return self.__class__(other + self.data) - return self.__class__(str(other) + self.data) - - def __mul__(self, n): - return self.__class__(self.data * n) - - __rmul__ = __mul__ - - def __mod__(self, args): - return self.__class__(self.data % args) - - def __rmod__(self, template): - return self.__class__(str(template) % self) - - # the following methods are defined in alphabetical order: - def capitalize(self): - return self.__class__(self.data.capitalize()) - - def casefold(self): - return self.__class__(self.data.casefold()) - - def center(self, width, *args): - return self.__class__(self.data.center(width, *args)) - - def count(self, sub, start=0, end=_sys.maxsize): - if isinstance(sub, UserString): - sub = sub.data - return self.data.count(sub, start, end) - - def removeprefix(self, prefix, /): - if isinstance(prefix, UserString): - prefix = prefix.data - return self.__class__(self.data.removeprefix(prefix)) - - def removesuffix(self, suffix, /): - if isinstance(suffix, UserString): - suffix = suffix.data - return self.__class__(self.data.removesuffix(suffix)) - - def encode(self, encoding='utf-8', errors='strict'): - encoding = 'utf-8' if encoding is None else encoding - errors = 'strict' if errors is None else errors - return self.data.encode(encoding, errors) - - def endswith(self, suffix, start=0, end=_sys.maxsize): - return self.data.endswith(suffix, start, end) - - def expandtabs(self, tabsize=8): - return self.__class__(self.data.expandtabs(tabsize)) - - def find(self, sub, start=0, end=_sys.maxsize): - if isinstance(sub, UserString): - sub = sub.data - return self.data.find(sub, start, end) - - def format(self, /, *args, **kwds): - return self.data.format(*args, **kwds) - - def format_map(self, mapping): - return self.data.format_map(mapping) - - def index(self, sub, start=0, end=_sys.maxsize): - return self.data.index(sub, start, end) - - def isalpha(self): - return self.data.isalpha() - - def isalnum(self): - return self.data.isalnum() - - def isascii(self): - return self.data.isascii() - - def isdecimal(self): - return self.data.isdecimal() - - def isdigit(self): - return self.data.isdigit() - - def isidentifier(self): - return self.data.isidentifier() - - def islower(self): - return self.data.islower() - - def isnumeric(self): - return self.data.isnumeric() - - def isprintable(self): - return self.data.isprintable() - - def isspace(self): - return self.data.isspace() - - def istitle(self): - return self.data.istitle() - - def isupper(self): - return self.data.isupper() - - def join(self, seq): - return self.data.join(seq) - - def ljust(self, width, *args): - return self.__class__(self.data.ljust(width, *args)) - - def lower(self): - return self.__class__(self.data.lower()) - - def lstrip(self, chars=None): - return self.__class__(self.data.lstrip(chars)) - - maketrans = str.maketrans - - def partition(self, sep): - return self.data.partition(sep) - - def replace(self, old, new, maxsplit=-1): - if isinstance(old, UserString): - old = old.data - if isinstance(new, UserString): - new = new.data - return self.__class__(self.data.replace(old, new, maxsplit)) - - def rfind(self, sub, start=0, end=_sys.maxsize): - if isinstance(sub, UserString): - sub = sub.data - return self.data.rfind(sub, start, end) - - def rindex(self, sub, start=0, end=_sys.maxsize): - return self.data.rindex(sub, start, end) - - def rjust(self, width, *args): - return self.__class__(self.data.rjust(width, *args)) - - def rpartition(self, sep): - return self.data.rpartition(sep) - - def rstrip(self, chars=None): - return self.__class__(self.data.rstrip(chars)) - - def split(self, sep=None, maxsplit=-1): - return self.data.split(sep, maxsplit) - - def rsplit(self, sep=None, maxsplit=-1): - return self.data.rsplit(sep, maxsplit) - - def splitlines(self, keepends=False): - return self.data.splitlines(keepends) - - def startswith(self, prefix, start=0, end=_sys.maxsize): - return self.data.startswith(prefix, start, end) - - def strip(self, chars=None): - return self.__class__(self.data.strip(chars)) - - def swapcase(self): - return self.__class__(self.data.swapcase()) - - def title(self): - return self.__class__(self.data.title()) - - def translate(self, *args): - return self.__class__(self.data.translate(*args)) - - def upper(self): - return self.__class__(self.data.upper()) - - def zfill(self, width): - return self.__class__(self.data.zfill(width)) \ No newline at end of file From 1a4ed51290106f88410ab8c6a3a038616b89c492 Mon Sep 17 00:00:00 2001 From: Nadica Rizova Date: Mon, 7 Aug 2023 09:45:08 +0200 Subject: [PATCH 176/213] changed alpine image to 3.13.7 --- images/ckan/2.9/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 00fea9b..5fa9020 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -1,7 +1,7 @@ ################## ### Build CKAN ### ################## -FROM alpine:3.15 as ckanbuild +FROM alpine:3.13.7 as ckanbuild # Used by Github Actions to tag the image with ENV IMAGE_TAG=2.9.9 @@ -70,7 +70,7 @@ RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.12.0 greenlet==1.1.3 ########################### ### Default-Extensions #### ########################### -FROM alpine:3.15 as extbuild +FROM alpine:3.13.7 as extbuild # Set src dirs ENV SRC_DIR=/srv/app/src @@ -105,7 +105,7 @@ RUN pip wheel --wheel-dir=/wheels git+${ENVVARS_GIT_URL}@${ENVVARS_GIT_BRANCH}#e ############ ### MAIN ### ############ -FROM alpine:3.15 +FROM alpine:3.13.7 LABEL maintainer="Keitaro Inc " LABEL org.opencontainers.image.source https://github.com/keitaroinc/docker-ckan From 63d1126c262b27077be87a9b1f080e42e223349f Mon Sep 17 00:00:00 2001 From: Daniel Ribeiro Date: Tue, 26 Sep 2023 06:01:09 +0000 Subject: [PATCH 177/213] Restore PyYAML 5.4.1 to avoid TypeError in webassets 0.12.1 CKAN 2.9.9 requires webassets 0.12.1, which calls yaml.load() without a Loader. This generated a warning in pyyaml 5.4.1, but raises a TypeError in 6.x. --- images/ckan/2.9/Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 5fa9020..535f27e 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -60,10 +60,11 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN cd ${SRC_DIR} && ls -lah ${SRC_DIR} && ash ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git -#### PATCH SINCE CYTON UPDATED TO 3.0.0 ### -RUN sed -i 's/pyyaml==5.4.1/pyyaml>=6.0.1/g' ckan/requirements.txt +# Create a constraint file that limits the Cython version to a compatible one, see https://github.com/yaml/pyyaml/issues/736 +RUN echo 'Cython < 3.0' > /tmp/constraint.txt +RUN PIP_CONSTRAINT=/tmp/constraint.txt pip wheel --wheel-dir=/wheels PyYAML==5.4.1 -# RUN pip-compile ckan/requirements.in +# RUN pip-compile ckan/requirements.in RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.12.0 greenlet==1.1.3 From 9233fdc829829cb65320d5101d50bb921ecad303 Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Mon, 16 Oct 2023 14:14:31 +0200 Subject: [PATCH 178/213] upgrade sqlalchemy version --- images/ckan/2.9/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 535f27e..5d5d5ba 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -179,6 +179,7 @@ RUN pip install -e /srv/app/src/ckan && \ # Change ownership to app user chown -R ckan:ckan /srv/app +RUN pip install sqlalchemy==1.3.19 # Remove wheels RUN rm -rf /srv/app/wheels /srv/app/ext_wheels From b86348ab066a8c4f6ce0d2ae88c0308e0025c1e4 Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Tue, 21 Nov 2023 16:07:48 +0100 Subject: [PATCH 179/213] api-tokens fix, two different tokens secrets were generated for encode and decode --- images/ckan/2.9/setup/app/start_ckan.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/images/ckan/2.9/setup/app/start_ckan.sh b/images/ckan/2.9/setup/app/start_ckan.sh index 58dffd7..99f09db 100755 --- a/images/ckan/2.9/setup/app/start_ckan.sh +++ b/images/ckan/2.9/setup/app/start_ckan.sh @@ -26,8 +26,10 @@ if grep -E "beaker.session.secret ?= ?$" $APP_DIR/production.ini then echo "Setting secrets in ini file" ckan config-tool $APP_DIR/production.ini "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" - ckan config-tool $APP_DIR/production.ini "api_token.jwt.encode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" - ckan config-tool $APP_DIR/production.ini "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" + ckan config-tool $APP_DIR/production.ini "WTF_CSRF_SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" + JWT_SECRET=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())') + ckan config-tool $APP_DIR/production.ini "api_token.jwt.encode.secret=$JWT_SECRET" + ckan config-tool $APP_DIR/production.ini "api_token.jwt.decode.secret=$JWT_SECRET" fi # Run the prerun script to init CKAN and create the default admin user From 0231df73f8a0982a81c6780a303f625dd08543d4 Mon Sep 17 00:00:00 2001 From: Kiril-Poposki1998 Date: Thu, 7 Dec 2023 11:52:43 +0100 Subject: [PATCH 180/213] ckan 2.10 github actions for pull request and push to master --- .github/workflows/master_merge.yml | 65 ++++++++++++++++++++++++++++++ .github/workflows/pr_checks.yml | 51 +++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index 37aaf2d..f43d916 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -6,6 +6,71 @@ on: jobs: + build-ckan-2-10: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.CR_PAT }} + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: | + /tmp/.buildx-cache-alpine-2-10 + /tmp/.buildx-cache-ubuntu-2-10 + key: ${{ runner.os }}-buildx-2-10-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-2-10 + + - name: Get docker tag for Alpine image + id: alpine + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile)" + + - name: Build and push CKAN 2.10 alpine + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.10 + file: ./images/ckan/2.10/Dockerfile + push: true + tags: | + keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + ghcr.io/keitaroinc/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-10 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-10 + + - name: Get docker tag for Ubuntu image + id: ubuntu + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile.focal)" + + - name: Build and push CKAN 2.10 ubuntu + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.10 + file: ./images/ckan/2.10/Dockerfile.focal + push: true + tags: | + keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + ghcr.io/keitaroinc/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-10 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-10 + build-ckan-2-9: runs-on: ubuntu-20.04 steps: diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 4619032..9990320 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -6,6 +6,57 @@ on: jobs: + build-ckan-2-10: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: | + /tmp/.buildx-cache-alpine-2-10 + /tmp/.buildx-cache-ubuntu-2-10 + key: ${{ runner.os }}-buildx-2-10-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx-2-10 + + - name: Get docker tag for Alpine image + id: alpine + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile)" + + - name: Build CKAN 2.10 alpine + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.10 + file: ./images/ckan/2.10/Dockerfile + push: false + tags: keitaro/ckan:${{ steps.alpine.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache-alpine-2-10 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-alpine-2-10 + + - name: Get docker tag for Ubuntu image + id: ubuntu + run: | + echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile.focal)" + + - name: Build CKAN 2.10 ubuntu + uses: docker/build-push-action@v2 + with: + context: ./images/ckan/2.10 + file: ./images/ckan/2.10/Dockerfile.focal + push: false + tags: keitaro/ckan:${{ steps.ubuntu.outputs.IMAGE_TAG }} + cache-from: type=local,src=/tmp/.buildx-cache-ubuntu-2-10 + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-ubuntu-2-10 + build-args: | + --progress=plain + + build-ckan-2-9: runs-on: ubuntu-20.04 steps: From 1bea0e7e6cdaaf57f65944731bf2e73d13946ebe Mon Sep 17 00:00:00 2001 From: Kiril-Poposki1998 Date: Wed, 13 Dec 2023 11:45:46 +0100 Subject: [PATCH 181/213] ckan 2.10 dockerfile update and scripts along with docker compose setup --- compose/2.10/.ckan-env | 6 +++--- compose/2.10/.env | 6 +++--- compose/2.10/docker-compose.yml | 2 +- compose/2.10/solr8/ckan_init_solr.sh | 2 +- images/ckan/2.10/Dockerfile | 8 ++++---- images/ckan/2.10/Dockerfile.focal | 7 +++---- 6 files changed, 15 insertions(+), 16 deletions(-) diff --git a/compose/2.10/.ckan-env b/compose/2.10/.ckan-env index 5ad12d2..b9f2598 100644 --- a/compose/2.10/.ckan-env +++ b/compose/2.10/.ckan-env @@ -7,8 +7,8 @@ MAINTENANCE_MODE=false # General Settings CKAN_SITE_ID=default -CKAN_SITE_URL=http://localhost:5005 -CKAN_PORT=5005 +CKAN_SITE_URL=http://localhost:5000 +CKAN_PORT=5000 CKAN_MAX_UPLOAD_SIZE_MB=20 CKAN___BEAKER__SESSION__SECRET=CHANGE_ME # See https://docs.ckan.org/en/latest/maintaining/configuration.html#api-token-settings @@ -38,5 +38,5 @@ CKAN__DATAPUSHER__API_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ6M0l # CKAN__DATAPUSHER__API_TOKEN=replace_this_with_api_token_once_ckan_starts # Solr configuration -CKAN_VERSION=2.9.7 +CKAN_VERSION=2.10 CKAN_CORE_NAME=ckan \ No newline at end of file diff --git a/compose/2.10/.env b/compose/2.10/.env index 3a722e6..b9e1c2d 100644 --- a/compose/2.10/.env +++ b/compose/2.10/.env @@ -12,10 +12,10 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.7 +CKAN_VERSION=2.10 CKAN_SITE_ID=default -CKAN_SITE_URL=http://localhost:5005 -CKAN_PORT=5005 +CKAN_SITE_URL=http://localhost:5000 +CKAN_PORT=5000 CKAN_MAX_UPLOAD_SIZE_MB=10 # Datapusher diff --git a/compose/2.10/docker-compose.yml b/compose/2.10/docker-compose.yml index bcd3f60..ccf8daf 100644 --- a/compose/2.10/docker-compose.yml +++ b/compose/2.10/docker-compose.yml @@ -1,5 +1,5 @@ # docker-compose build && docker-compose up -d -version: "3" +version: "3.8" volumes: ckan_data: diff --git a/compose/2.10/solr8/ckan_init_solr.sh b/compose/2.10/solr8/ckan_init_solr.sh index 8ea06c3..296d52b 100755 --- a/compose/2.10/solr8/ckan_init_solr.sh +++ b/compose/2.10/solr8/ckan_init_solr.sh @@ -8,7 +8,7 @@ set -e -CKAN_SOLR_SCHEMA_URL=https://raw.githubusercontent.com/ckan/ckan/ckan-$CKAN_VERSION/ckan/config/solr/schema.solr8.xml +CKAN_SOLR_SCHEMA_URL=https://raw.githubusercontent.com/ckan/ckan/$CKAN_VERSION/ckan/config/solr/schema.xml echo "Check whether managed schema exists for CKAN $CKAN_VERSION" if ! curl --output /dev/null --silent --head --fail "$CKAN_SOLR_SCHEMA_URL"; then diff --git a/images/ckan/2.10/Dockerfile b/images/ckan/2.10/Dockerfile index becda88..b5e00cc 100644 --- a/images/ckan/2.10/Dockerfile +++ b/images/ckan/2.10/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.17.2 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.10.0 +ENV IMAGE_TAG=2.10.2 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.10.0 +ENV GIT_BRANCH=ckan-2.10.2 # Set src dirs ENV SRC_DIR=/srv/app/src @@ -57,7 +57,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh # RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.12.0 greenlet==1.1.3 +RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==22.10.2 greenlet==2.0.2 ########################### @@ -136,7 +136,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uWSGI==2.0.20 gevent==21.12.0 +RUN pip install --no-index --find-links=/srv/app/wheels uWSGI==2.0.20 gevent==22.10.2 # Create a local user and group to run the app RUN addgroup -g 92 -S ckan && \ diff --git a/images/ckan/2.10/Dockerfile.focal b/images/ckan/2.10/Dockerfile.focal index 369d7fd..7c2e867 100644 --- a/images/ckan/2.10/Dockerfile.focal +++ b/images/ckan/2.10/Dockerfile.focal @@ -8,7 +8,7 @@ ENV IMAGE_TAG=2.9.7-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.7 +ENV GIT_BRANCH=ckan-2.10.2 # Set timezone ENV TZ=UTC @@ -81,8 +81,7 @@ COPY ./scripts/apply_ckan_patches.sh ${SRC_DIR}/apply_ckan_patches.sh RUN ${SRC_DIR}/apply_ckan_patches.sh RUN rm -rf /srv/app/src/ckan/.git RUN pip wheel --wheel-dir=/wheels -r ckan/requirements.txt -RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==21.12.0 greenlet==1.1.3 - +RUN pip wheel --wheel-dir=/wheels uWSGI==2.0.20 gevent==22.10.2 greenlet==2.0.2 ########################### ### Default-Extensions #### @@ -192,7 +191,7 @@ COPY --from=extbuild /wheels /srv/app/ext_wheels COPY --from=ckanbuild /srv/app/src/ckan ${CKAN_DIR} # Additional install steps for build stages artifacts -RUN pip install --no-index --find-links=/srv/app/wheels uWSGI==2.0.20 gevent==21.12.0 +RUN pip install --no-index --find-links=/srv/app/wheels uWSGI==2.0.20 gevent==22.10.2 # Create a local user and group to run the app RUN groupadd -g 92 ckan && \ From efe53d5f560f7d5eae4ce9341b25a63f1f8d33cd Mon Sep 17 00:00:00 2001 From: Kiril-Poposki1998 Date: Tue, 19 Dec 2023 11:09:52 +0100 Subject: [PATCH 182/213] change image in docker compose and version in env file --- compose/2.10/.env | 2 +- compose/2.10/docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compose/2.10/.env b/compose/2.10/.env index b9e1c2d..ef7cf75 100644 --- a/compose/2.10/.env +++ b/compose/2.10/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.10 +CKAN_VERSION=2.10.2 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/2.10/docker-compose.yml b/compose/2.10/docker-compose.yml index ccf8daf..00b40d2 100644 --- a/compose/2.10/docker-compose.yml +++ b/compose/2.10/docker-compose.yml @@ -9,7 +9,7 @@ volumes: services: ckan: container_name: ckan - build: ../../images/ckan/2.10/ + image: ghcr.io/keitaroinc/ckan:2.10.2 networks: - frontend - backend From e7711d51ab0f82cd6f0643111d2db5a7141108d0 Mon Sep 17 00:00:00 2001 From: Kiril-Poposki1998 Date: Tue, 19 Dec 2023 11:23:57 +0100 Subject: [PATCH 183/213] add beaker secrets to start ckan --- compose/2.10/.env | 2 +- images/ckan/2.10/setup/app/start_ckan.sh | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/compose/2.10/.env b/compose/2.10/.env index ef7cf75..b9e1c2d 100644 --- a/compose/2.10/.env +++ b/compose/2.10/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.10.2 +CKAN_VERSION=2.10 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/images/ckan/2.10/setup/app/start_ckan.sh b/images/ckan/2.10/setup/app/start_ckan.sh index f1b5986..abf4473 100755 --- a/images/ckan/2.10/setup/app/start_ckan.sh +++ b/images/ckan/2.10/setup/app/start_ckan.sh @@ -12,12 +12,24 @@ then done fi +# Add session secret from chart +if [[ -z $BEAKER_SESSION_SECRET || -v $BEAKER_SESSION_SECRET || -z $JWT_ENCODE_SECRET || -v $JWT_ENCODE_SECRET || -z $JWT_DECODE_SECRET || -v $JWT_DECODE_SECRET ]];then + echo "Not all environment variables are set. Generating sessions..." +else + echo "Setting session secrets from environment variables" + ckan config-tool $APP_DIR/production.ini "beaker.session.secret=$BEAKER_SESSION_SECRET" + ckan config-tool $APP_DIR/production.ini "api_token.jwt.encode.secret=$JWT_ENCODE_SECRET" + ckan config-tool $APP_DIR/production.ini "api_token.jwt.decode.secret=$JWT_DECODE_SECRET" +fi + if grep -E "beaker.session.secret ?= ?$" $APP_DIR/production.ini then echo "Setting secrets in ini file" ckan config-tool $APP_DIR/production.ini "beaker.session.secret=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" - ckan config-tool $APP_DIR/production.ini "api_token.jwt.encode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" - ckan config-tool $APP_DIR/production.ini "api_token.jwt.decode.secret=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())')" + ckan config-tool $APP_DIR/production.ini "WTF_CSRF_SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_urlsafe())')" + JWT_SECRET=$(python3 -c 'import secrets; print("string:" + secrets.token_urlsafe())') + ckan config-tool $APP_DIR/production.ini "api_token.jwt.encode.secret=$JWT_SECRET" + ckan config-tool $APP_DIR/production.ini "api_token.jwt.decode.secret=$JWT_SECRET" fi echo "Starting UWSGI with '${UWSGI_PROC_NO:-2}' workers" From d4bcb719f07d4675e3d1af16c3623cc863163bca Mon Sep 17 00:00:00 2001 From: Kiril-Poposki1998 Date: Wed, 20 Dec 2023 12:36:53 +0100 Subject: [PATCH 184/213] update focal image tag --- images/ckan/2.10/Dockerfile.focal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/ckan/2.10/Dockerfile.focal b/images/ckan/2.10/Dockerfile.focal index 7c2e867..5fc764f 100644 --- a/images/ckan/2.10/Dockerfile.focal +++ b/images/ckan/2.10/Dockerfile.focal @@ -4,7 +4,7 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.7-focal +ENV IMAGE_TAG=2.10.2-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git From 12c7519e7ed28459607708c9070efb1891813afd Mon Sep 17 00:00:00 2001 From: Kiril-Poposki1998 Date: Thu, 21 Dec 2023 10:57:06 +0100 Subject: [PATCH 185/213] new release 2.10.3 --- images/ckan/2.10/Dockerfile | 4 ++-- images/ckan/2.10/Dockerfile.focal | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/images/ckan/2.10/Dockerfile b/images/ckan/2.10/Dockerfile index b5e00cc..e7140ef 100644 --- a/images/ckan/2.10/Dockerfile +++ b/images/ckan/2.10/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.17.2 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.10.2 +ENV IMAGE_TAG=2.10.3 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.10.2 +ENV GIT_BRANCH=ckan-2.10.3 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.10/Dockerfile.focal b/images/ckan/2.10/Dockerfile.focal index 5fc764f..6aa802c 100644 --- a/images/ckan/2.10/Dockerfile.focal +++ b/images/ckan/2.10/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.10.2-focal +ENV IMAGE_TAG=2.10.3-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.10.2 +ENV GIT_BRANCH=ckan-2.10.3 # Set timezone ENV TZ=UTC From 916824d98768cabafcfad04015a6691c4bee553b Mon Sep 17 00:00:00 2001 From: Kiril-Poposki1998 Date: Thu, 21 Dec 2023 11:11:39 +0100 Subject: [PATCH 186/213] new release 2.9.10 --- images/ckan/2.9/Dockerfile | 4 ++-- images/ckan/2.9/Dockerfile.focal | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index 5d5d5ba..c1423d3 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.13.7 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.9 +ENV IMAGE_TAG=2.9.10 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.9 +ENV GIT_BRANCH=ckan-2.9.10 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 53adcc0..336d7bc 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.9-focal +ENV IMAGE_TAG=2.9.10-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.9 +ENV GIT_BRANCH=ckan-2.9.10 # Set timezone ENV TZ=UTC From 96094e02a9590d635428ebe6fc0b09ea41feb571 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Mon, 15 Jan 2024 13:12:53 +0100 Subject: [PATCH 187/213] added trivy in repo scan mode --- .github/workflows/trivy_scan.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/trivy_scan.yml diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml new file mode 100644 index 0000000..a69b0f4 --- /dev/null +++ b/.github/workflows/trivy_scan.yml @@ -0,0 +1,31 @@ +name: build +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '28 3 * * 5' + +jobs: + build: + name: Build + runs-on: ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Run Trivy vulnerability scanner in repo mode + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + ignore-unfixed: true + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' \ No newline at end of file From 009028b4d9518797162354b38e1e9a742c4407a3 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Mon, 15 Jan 2024 13:14:24 +0100 Subject: [PATCH 188/213] changed ci name --- .github/workflows/trivy_scan.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml index a69b0f4..af3a83c 100644 --- a/.github/workflows/trivy_scan.yml +++ b/.github/workflows/trivy_scan.yml @@ -1,4 +1,4 @@ -name: build +name: Repo Scan on: push: branches: [ "master" ] @@ -10,7 +10,7 @@ on: jobs: build: - name: Build + name: repo_scan runs-on: ubuntu-20.04 steps: - name: Checkout code From e76e34bd92593ddb2ed17f6a4cfbeb5c950d5f08 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Mon, 15 Jan 2024 13:24:05 +0100 Subject: [PATCH 189/213] added docker scan mode --- .github/workflows/trivy_scan.yml | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml index af3a83c..137f92e 100644 --- a/.github/workflows/trivy_scan.yml +++ b/.github/workflows/trivy_scan.yml @@ -9,7 +9,7 @@ on: - cron: '28 3 * * 5' jobs: - build: + repo_scan: name: repo_scan runs-on: ubuntu-20.04 steps: @@ -25,6 +25,31 @@ jobs: output: 'trivy-results.sarif' severity: 'CRITICAL' + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' + + + docker_scan: + name: docker_scan + runs-on: ubuntu-20.04 + steps: + + - name: Checkout code + uses: actions/checkout@v3 + + - name: Build an image from Dockerfile + run: | + docker build -t keitaro/ckan/2.10:${{ github.sha }} -f ./images/ckan/2.10/Dockerfile . + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: 'keitaro/ckan/2.10:${{ github.sha }}' + format: 'sarif' + output: 'trivy-results.sarif' + - name: Upload Trivy scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@v2 with: From 50eeb66a5bd9281c20ff0887528b525b3215c64c Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Mon, 15 Jan 2024 13:45:40 +0100 Subject: [PATCH 190/213] changed docker build tar --- .github/workflows/trivy_scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml index 137f92e..1020856 100644 --- a/.github/workflows/trivy_scan.yml +++ b/.github/workflows/trivy_scan.yml @@ -41,7 +41,7 @@ jobs: - name: Build an image from Dockerfile run: | - docker build -t keitaro/ckan/2.10:${{ github.sha }} -f ./images/ckan/2.10/Dockerfile . + docker build -t keitaro/ckan/2.10:${{ github.sha }} -f ./images/ckan/2.10/Dockerfile ./images/ckan/2.10/ - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master From 232f7de6aca39f23a6c99f45893cf23cc05165c0 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Mon, 15 Jan 2024 13:54:34 +0100 Subject: [PATCH 191/213] added ckan versions to be scanned --- .github/workflows/trivy_scan.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml index 1020856..5d51dfb 100644 --- a/.github/workflows/trivy_scan.yml +++ b/.github/workflows/trivy_scan.yml @@ -42,11 +42,14 @@ jobs: - name: Build an image from Dockerfile run: | docker build -t keitaro/ckan/2.10:${{ github.sha }} -f ./images/ckan/2.10/Dockerfile ./images/ckan/2.10/ + docker build -t keitaro/ckan/2.10-focal:${{ github.sha }} -f ./images/ckan/2.10/Dockerfile.focal ./images/ckan/2.10/ + docker build -t keitaro/ckan/2.9:${{ github.sha }} -f ./images/ckan/2.9/Dockerfile ./images/ckan/2.9/ + docker build -t keitaro/ckan/2.9-focal:${{ github.sha }} -f ./images/ckan/2.9/Dockerfile.focal ./images/ckan/2.9/ - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: - image-ref: 'keitaro/ckan/2.10:${{ github.sha }}' + image-ref: 'keitaro/ckan/2.10:${{ github.sha }},keitaro/ckan/2.10-focal:${{ github.sha }}' format: 'sarif' output: 'trivy-results.sarif' From efdf8f19f986c3ebf04619839a23d4e6b97d703f Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Mon, 15 Jan 2024 16:21:57 +0100 Subject: [PATCH 192/213] added more actions for 2.9 and 2.10 ckan versions --- .github/workflows/trivy_scan.yml | 76 ++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml index 5d51dfb..e7e182f 100644 --- a/.github/workflows/trivy_scan.yml +++ b/.github/workflows/trivy_scan.yml @@ -31,8 +31,8 @@ jobs: sarif_file: 'trivy-results.sarif' - docker_scan: - name: docker_scan + scan_2_10: + name: scan_2_10 runs-on: ubuntu-20.04 steps: @@ -42,14 +42,84 @@ jobs: - name: Build an image from Dockerfile run: | docker build -t keitaro/ckan/2.10:${{ github.sha }} -f ./images/ckan/2.10/Dockerfile ./images/ckan/2.10/ + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: 'keitaro/ckan/2.10:${{ github.sha }}' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' + + + scan_2_10_focal: + name: scan_2_10_focal + runs-on: ubuntu-20.04 + steps: + + - name: Checkout code + uses: actions/checkout@v3 + + - name: Build an image from Dockerfile + run: | docker build -t keitaro/ckan/2.10-focal:${{ github.sha }} -f ./images/ckan/2.10/Dockerfile.focal ./images/ckan/2.10/ + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: 'keitaro/ckan/2.10-focal:${{ github.sha }}' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' + + scan_2_9: + name: scan_2_9 + runs-on: ubuntu-20.04 + steps: + + - name: Checkout code + uses: actions/checkout@v3 + + - name: Build an image from Dockerfile + run: | docker build -t keitaro/ckan/2.9:${{ github.sha }} -f ./images/ckan/2.9/Dockerfile ./images/ckan/2.9/ + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: 'keitaro/ckan/2.9:${{ github.sha }}' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' + + scan_2_9_focal: + name: scan_2_9_focal + runs-on: ubuntu-20.04 + steps: + + - name: Checkout code + uses: actions/checkout@v3 + + - name: Build an image from Dockerfile + run: | docker build -t keitaro/ckan/2.9-focal:${{ github.sha }} -f ./images/ckan/2.9/Dockerfile.focal ./images/ckan/2.9/ - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: - image-ref: 'keitaro/ckan/2.10:${{ github.sha }},keitaro/ckan/2.10-focal:${{ github.sha }}' + image-ref: 'keitaro/ckan/2.9-focal:${{ github.sha }}' format: 'sarif' output: 'trivy-results.sarif' From 1be892cab9e7e5a8a5e511e7362c446eca6a8ff6 Mon Sep 17 00:00:00 2001 From: "filip.mihajlovski" Date: Mon, 15 Jan 2024 16:39:34 +0100 Subject: [PATCH 193/213] added scans for datapusher and psql-init --- .github/workflows/trivy_scan.yml | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml index e7e182f..6f8c917 100644 --- a/.github/workflows/trivy_scan.yml +++ b/.github/workflows/trivy_scan.yml @@ -123,6 +123,55 @@ jobs: format: 'sarif' output: 'trivy-results.sarif' + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' + + scan_psql_init: + name: scan_psql_init + runs-on: ubuntu-20.04 + steps: + + - name: Checkout code + uses: actions/checkout@v3 + + - name: Build an image from Dockerfile + run: | + docker build -t keitaro/psql-init:${{ github.sha }} -f ./images/psql-init/Dockerfile ./images/psql-init/ + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: 'keitaro/psql-init:${{ github.sha }}' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' + + + scan_datapusher: + name: scan_datapusher + runs-on: ubuntu-20.04 + steps: + + - name: Checkout code + uses: actions/checkout@v3 + + - name: Build an image from Dockerfile + run: | + docker build -t keitaro/datapusher:${{ github.sha }} -f ./images/datapusher/Dockerfile ./images/datapusher/ + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: 'keitaro/datapusher:${{ github.sha }}' + format: 'sarif' + output: 'trivy-results.sarif' + - name: Upload Trivy scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@v2 with: From 8e78f7c0938eb0111e88b0d31b05e323acbcb189 Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Wed, 17 Jan 2024 11:34:14 +0100 Subject: [PATCH 194/213] added ckan.datapusher.api_token in 2.10 --- images/ckan/2.10/setup/app/start_ckan.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/images/ckan/2.10/setup/app/start_ckan.sh b/images/ckan/2.10/setup/app/start_ckan.sh index abf4473..0435142 100755 --- a/images/ckan/2.10/setup/app/start_ckan.sh +++ b/images/ckan/2.10/setup/app/start_ckan.sh @@ -1,4 +1,8 @@ #!/bin/bash + +# Add ckan.datapusher.api_token to the CKAN config file (updated with corrected value later) +ckan config-tool $APP_DIR/production.ini ckan.datapusher.api_token=xxx + # Run any startup scripts provided by images extending this one if [[ -d "${APP_DIR}/docker-entrypoint.d" ]] then @@ -38,6 +42,9 @@ UWSGI_OPTS="--socket /tmp/uwsgi.sock --uid ckan --gid ckan --http :5000 --master # Run the prerun script to init CKAN and create the default admin user python prerun.py || { echo '[CKAN prerun] FAILED. Exiting...' ; exit 1; } +echo "Set up ckan.datapusher.api_token in the CKAN config file" +ckan config-tool $APP_DIR/production.ini "ckan.datapusher.api_token=$(ckan -c $APP_DIR/production.ini user token add ckan_admin datapusher | tail -n 1 | tr -d '\t')" + # Check if we are in maintenance mode and if yes serve the maintenance pages if [ "$MAINTENANCE_MODE" = true ]; then PYTHONUNBUFFERED=1 python maintenance/serve.py; fi From ad9d3b5a6adc1be40701067a85ea8a6965c8dde4 Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Tue, 5 Mar 2024 10:35:58 +0100 Subject: [PATCH 195/213] update actions versions --- .github/workflows/master_merge.yml | 80 +++++++++++++++--------------- .github/workflows/pr_checks.yml | 56 ++++++++++----------- .github/workflows/trivy_scan.yml | 14 +++--- 3 files changed, 75 insertions(+), 75 deletions(-) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index a5aa63e..4cf2286 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -9,26 +9,26 @@ jobs: build-ckan-2-10: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.CR_PAT }} - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | /tmp/.buildx-cache-alpine-2-10 @@ -43,7 +43,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile)" - name: Build and push CKAN 2.10 alpine - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.10 file: ./images/ckan/2.10/Dockerfile @@ -60,7 +60,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile.focal)" - name: Build and push CKAN 2.10 ubuntu - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.10 file: ./images/ckan/2.10/Dockerfile.focal @@ -74,26 +74,26 @@ jobs: build-ckan-2-9: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.CR_PAT }} - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | /tmp/.buildx-cache-alpine-2-9 @@ -108,7 +108,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile)" - name: Build and push CKAN 2.9 alpine - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.9 file: ./images/ckan/2.9/Dockerfile @@ -125,7 +125,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile.focal)" - name: Build and push CKAN 2.9 ubuntu - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.9 file: ./images/ckan/2.9/Dockerfile.focal @@ -139,26 +139,26 @@ jobs: build-ckan-2-8: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.CR_PAT }} - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | /tmp/.buildx-cache-alpine-2-8 @@ -173,7 +173,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile)" - name: Build and push CKAN 2.8 alpine - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.8 file: ./images/ckan/2.8/Dockerfile @@ -190,7 +190,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile.focal)" - name: Build and push CKAN 2.8 ubuntu - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.8 file: ./images/ckan/2.8/Dockerfile.focal @@ -204,26 +204,26 @@ jobs: build-ckan-2-7: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.CR_PAT }} - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | /tmp/.buildx-cache-alpine-2-7 @@ -238,7 +238,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile)" - name: Build and push CKAN 2.7 alpine - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.7 file: ./images/ckan/2.7/Dockerfile @@ -255,7 +255,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile.focal)" - name: Build CKAN 2.7 ubuntu - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.7 file: ./images/ckan/2.7/Dockerfile.focal @@ -269,26 +269,26 @@ jobs: build-ckan-datapusher: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.CR_PAT }} - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: /tmp/.buildx-cache-datapusher key: ${{ runner.os }}-buildx-datapusher-${{ github.sha }} @@ -301,7 +301,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/datapusher/Dockerfile)" - name: Build and push CKAN datapusher - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/datapusher file: ./images/datapusher/Dockerfile @@ -315,26 +315,26 @@ jobs: build-psql-init: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.CR_PAT }} - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: /tmp/.buildx-cache-psql-init key: ${{ runner.os }}-buildx-psql-init-${{ github.sha }} @@ -347,7 +347,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/psql-init/Dockerfile)" - name: Build and push psql-init - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/psql-init file: ./images/psql-init/Dockerfile diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index f16959f..a09beb2 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -9,13 +9,13 @@ jobs: build-ckan-2-10: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | /tmp/.buildx-cache-alpine-2-10 @@ -30,7 +30,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile)" - name: Build CKAN 2.10 alpine - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.10 file: ./images/ckan/2.10/Dockerfile @@ -45,7 +45,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile.focal)" - name: Build CKAN 2.10 ubuntu - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.10 file: ./images/ckan/2.10/Dockerfile.focal @@ -60,13 +60,13 @@ jobs: build-ckan-2-9: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | /tmp/.buildx-cache-alpine-2-9 @@ -81,7 +81,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile)" - name: Build CKAN 2.9 alpine - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.9 file: ./images/ckan/2.9/Dockerfile @@ -96,7 +96,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile.focal)" - name: Build CKAN 2.9 ubuntu - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.9 file: ./images/ckan/2.9/Dockerfile.focal @@ -110,13 +110,13 @@ jobs: build-ckan-2-8: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | /tmp/.buildx-cache-alpine-2-8 @@ -131,7 +131,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile)" - name: Build CKAN 2.8 alpine - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.8 file: ./images/ckan/2.8/Dockerfile @@ -146,7 +146,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile.focal)" - name: Build CKAN 2.8 ubuntu - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.8 file: ./images/ckan/2.8/Dockerfile.focal @@ -158,13 +158,13 @@ jobs: build-ckan-2-7: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | /tmp/.buildx-cache-alpine-2-7 @@ -179,7 +179,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile)" - name: Build CKAN 2.7 alpine - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.7 file: ./images/ckan/2.7/Dockerfile @@ -194,7 +194,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile.focal)" - name: Build CKAN 2.7 ubuntu - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/ckan/2.7 file: ./images/ckan/2.7/Dockerfile.focal @@ -206,13 +206,13 @@ jobs: build-ckan-datapusher: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: /tmp/.buildx-cache-datapusher key: ${{ runner.os }}-buildx-datapusher-${{ github.sha }} @@ -225,7 +225,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/datapusher/Dockerfile)" - name: Build CKAN datapusher - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/datapusher file: ./images/datapusher/Dockerfile @@ -237,13 +237,13 @@ jobs: build-psql-init: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: /tmp/.buildx-cache-psql-init key: ${{ runner.os }}-buildx-psql-init-${{ github.sha }} @@ -256,7 +256,7 @@ jobs: echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/psql-init/Dockerfile)" - name: Build psql-init - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: ./images/psql-init file: ./images/psql-init/Dockerfile diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml index 6f8c917..99a4a11 100644 --- a/.github/workflows/trivy_scan.yml +++ b/.github/workflows/trivy_scan.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner in repo mode uses: aquasecurity/trivy-action@master @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build an image from Dockerfile run: | @@ -62,7 +62,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build an image from Dockerfile run: | @@ -86,7 +86,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build an image from Dockerfile run: | @@ -110,7 +110,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build an image from Dockerfile run: | @@ -134,7 +134,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build an image from Dockerfile run: | @@ -159,7 +159,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build an image from Dockerfile run: | From 11e1e0b169573ad4c589c8c28da3d3494b14627c Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Tue, 5 Mar 2024 11:52:43 +0100 Subject: [PATCH 196/213] change set-output to GITHUB_OUTPUT env variable --- .github/workflows/pr_checks.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index a09beb2..b17109d 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -27,7 +27,7 @@ jobs: - name: Get docker tag for Alpine image id: alpine run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile)" >> $GITHUB_OUTPUT - name: Build CKAN 2.10 alpine uses: docker/build-push-action@v5 @@ -42,7 +42,7 @@ jobs: - name: Get docker tag for Ubuntu image id: ubuntu run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile.focal)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile.focal)" >> $GITHUB_OUTPUT - name: Build CKAN 2.10 ubuntu uses: docker/build-push-action@v5 @@ -78,7 +78,7 @@ jobs: - name: Get docker tag for Alpine image id: alpine run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile)" >> $GITHUB_OUTPUT - name: Build CKAN 2.9 alpine uses: docker/build-push-action@v5 @@ -93,7 +93,7 @@ jobs: - name: Get docker tag for Ubuntu image id: ubuntu run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile.focal)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile.focal)" >> $GITHUB_OUTPUT - name: Build CKAN 2.9 ubuntu uses: docker/build-push-action@v5 @@ -128,7 +128,7 @@ jobs: - name: Get docker tag for Alpine image id: alpine run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile)" >> $GITHUB_OUTPUT - name: Build CKAN 2.8 alpine uses: docker/build-push-action@v5 @@ -143,7 +143,7 @@ jobs: - name: Get docker tag for Ubuntu image id: ubuntu run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile.focal)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile.focal)" >> $GITHUB_OUTPUT - name: Build CKAN 2.8 ubuntu uses: docker/build-push-action@v5 @@ -176,7 +176,7 @@ jobs: - name: Get docker tag for Alpine image id: alpine run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile)" >> $GITHUB_OUTPUT - name: Build CKAN 2.7 alpine uses: docker/build-push-action@v5 @@ -191,7 +191,7 @@ jobs: - name: Get docker tag for Ubuntu image id: ubuntu run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile.focal)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile.focal)" >> $GITHUB_OUTPUT - name: Build CKAN 2.7 ubuntu uses: docker/build-push-action@v5 @@ -222,7 +222,7 @@ jobs: - name: Get docker tag for datapusher image id: datapusher run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/datapusher/Dockerfile)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/datapusher/Dockerfile)" >> $GITHUB_OUTPUT - name: Build CKAN datapusher uses: docker/build-push-action@v5 @@ -253,7 +253,7 @@ jobs: - name: Get docker tag for psql-init image id: psql-init run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/psql-init/Dockerfile)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/psql-init/Dockerfile)" >> $GITHUB_OUTPUT - name: Build psql-init uses: docker/build-push-action@v5 From b3fec789405a8279babf417d094169256f655c47 Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Tue, 5 Mar 2024 14:01:05 +0100 Subject: [PATCH 197/213] change set-output to GITHUB_OUTPUT env variable, update actions/cache --- .github/workflows/master_merge.yml | 32 +++++++++++++++--------------- .github/workflows/pr_checks.yml | 12 +++++------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/master_merge.yml b/.github/workflows/master_merge.yml index 4cf2286..b521325 100644 --- a/.github/workflows/master_merge.yml +++ b/.github/workflows/master_merge.yml @@ -28,7 +28,7 @@ jobs: password: ${{ secrets.CR_PAT }} - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | /tmp/.buildx-cache-alpine-2-10 @@ -40,7 +40,7 @@ jobs: - name: Get docker tag for Alpine image id: alpine run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile)" >> $GITHUB_OUTPUT - name: Build and push CKAN 2.10 alpine uses: docker/build-push-action@v5 @@ -57,7 +57,7 @@ jobs: - name: Get docker tag for Ubuntu image id: ubuntu run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile.focal)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.10/Dockerfile.focal)" >> $GITHUB_OUTPUT - name: Build and push CKAN 2.10 ubuntu uses: docker/build-push-action@v5 @@ -93,7 +93,7 @@ jobs: password: ${{ secrets.CR_PAT }} - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | /tmp/.buildx-cache-alpine-2-9 @@ -105,7 +105,7 @@ jobs: - name: Get docker tag for Alpine image id: alpine run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile)" >> $GITHUB_OUTPUT - name: Build and push CKAN 2.9 alpine uses: docker/build-push-action@v5 @@ -122,7 +122,7 @@ jobs: - name: Get docker tag for Ubuntu image id: ubuntu run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile.focal)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.9/Dockerfile.focal)" >> $GITHUB_OUTPUT - name: Build and push CKAN 2.9 ubuntu uses: docker/build-push-action@v5 @@ -158,7 +158,7 @@ jobs: password: ${{ secrets.CR_PAT }} - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | /tmp/.buildx-cache-alpine-2-8 @@ -170,7 +170,7 @@ jobs: - name: Get docker tag for Alpine image id: alpine run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile)" >> $GITHUB_OUTPUT - name: Build and push CKAN 2.8 alpine uses: docker/build-push-action@v5 @@ -187,7 +187,7 @@ jobs: - name: Get docker tag for Ubuntu image id: ubuntu run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile.focal)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.8/Dockerfile.focal)" >> $GITHUB_OUTPUT - name: Build and push CKAN 2.8 ubuntu uses: docker/build-push-action@v5 @@ -223,7 +223,7 @@ jobs: password: ${{ secrets.CR_PAT }} - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | /tmp/.buildx-cache-alpine-2-7 @@ -235,7 +235,7 @@ jobs: - name: Get docker tag for Alpine image id: alpine run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile)" >> $GITHUB_OUTPUT - name: Build and push CKAN 2.7 alpine uses: docker/build-push-action@v5 @@ -252,7 +252,7 @@ jobs: - name: Get docker tag for Ubuntu image id: ubuntu run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile.focal)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/ckan/2.7/Dockerfile.focal)" >> $GITHUB_OUTPUT - name: Build CKAN 2.7 ubuntu uses: docker/build-push-action@v5 @@ -288,7 +288,7 @@ jobs: password: ${{ secrets.CR_PAT }} - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache-datapusher key: ${{ runner.os }}-buildx-datapusher-${{ github.sha }} @@ -298,7 +298,7 @@ jobs: - name: Get docker tag for datapusher image id: datapusher run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/datapusher/Dockerfile)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/datapusher/Dockerfile)" >> $GITHUB_OUTPUT - name: Build and push CKAN datapusher uses: docker/build-push-action@v5 @@ -334,7 +334,7 @@ jobs: password: ${{ secrets.CR_PAT }} - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache-psql-init key: ${{ runner.os }}-buildx-psql-init-${{ github.sha }} @@ -344,7 +344,7 @@ jobs: - name: Get docker tag for psql-init image id: psql-init run: | - echo "::set-output name=IMAGE_TAG::$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/psql-init/Dockerfile)" + echo "IMAGE_TAG=$(awk -F '=' '/IMAGE_TAG/{print $2}' ./images/psql-init/Dockerfile)" >> $GITHUB_OUTPUT - name: Build and push psql-init uses: docker/build-push-action@v5 diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index b17109d..0bad5a2 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -15,7 +15,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | /tmp/.buildx-cache-alpine-2-10 @@ -66,7 +66,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | /tmp/.buildx-cache-alpine-2-9 @@ -116,7 +116,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | /tmp/.buildx-cache-alpine-2-8 @@ -164,7 +164,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | /tmp/.buildx-cache-alpine-2-7 @@ -212,7 +212,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache-datapusher key: ${{ runner.os }}-buildx-datapusher-${{ github.sha }} @@ -243,7 +243,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache-psql-init key: ${{ runner.os }}-buildx-psql-init-${{ github.sha }} From 640c765fb0f0232c85c1bbd56b9d17ac7a944899 Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Tue, 5 Mar 2024 14:08:17 +0100 Subject: [PATCH 198/213] update codeql-action to v3 --- .github/workflows/trivy_scan.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/trivy_scan.yml b/.github/workflows/trivy_scan.yml index 99a4a11..8bce2d1 100644 --- a/.github/workflows/trivy_scan.yml +++ b/.github/workflows/trivy_scan.yml @@ -26,7 +26,7 @@ jobs: severity: 'CRITICAL' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' @@ -51,7 +51,7 @@ jobs: output: 'trivy-results.sarif' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' @@ -76,7 +76,7 @@ jobs: output: 'trivy-results.sarif' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' @@ -100,7 +100,7 @@ jobs: output: 'trivy-results.sarif' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' @@ -124,7 +124,7 @@ jobs: output: 'trivy-results.sarif' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' @@ -148,7 +148,7 @@ jobs: output: 'trivy-results.sarif' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' @@ -173,6 +173,6 @@ jobs: output: 'trivy-results.sarif' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' \ No newline at end of file From 0969b451217a57f05f7e3383bf35c40dbc5f119d Mon Sep 17 00:00:00 2001 From: Kiril-Poposki1998 Date: Thu, 7 Mar 2024 14:42:44 +0100 Subject: [PATCH 199/213] rewrite ckan 2.10 prerun script to connect to solr --- images/ckan/2.10/setup/app/prerun.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/images/ckan/2.10/setup/app/prerun.py b/images/ckan/2.10/setup/app/prerun.py index 64a56bc..8fad1f7 100644 --- a/images/ckan/2.10/setup/app/prerun.py +++ b/images/ckan/2.10/setup/app/prerun.py @@ -13,16 +13,16 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ +from multiprocessing import connection import os import sys import subprocess import psycopg2 from sqlalchemy.engine.url import make_url -import urllib.request, urllib.error, urllib.parse +import urllib.request, urllib.error, urllib.parse, base64 import re import json - import time ckan_ini = os.environ.get('CKAN_INI', '/srv/app/production.ini') @@ -70,20 +70,18 @@ def check_solr_connection(retry=None): sys.exit(1) url = os.environ.get('CKAN_SOLR_URL', '') - username = os.environ.get('SOLR_ADMIN_USERNAME', 'admin') - password = os.environ.get('SOLR_ADMIN_PASSWORD', 'pass') + username = os.environ.get('CKAN_SOLR_USER', '') + password = os.environ.get('CKAN_SOLR_PASSWORD', '') search_url = '{url}/schema/name?wt=json'.format(url=url) try: if not username: connection = urllib.request.urlopen(search_url) else: - passman = urllib.request.HTTPPasswordMgrWithDefaultRealm() - passman.add_password(None, search_url, username, password) - authhandler = urllib.request.HTTPBasicAuthHandler(passman) - opener = urllib.request.build_opener(authhandler) - urllib.request.install_opener(opener) - connection = urllib.request.urlopen(search_url) + request = urllib.request.Request(search_url) + base64string = base64.b64encode(bytes('%s:%s' % (username, password),'ascii')) + request.add_header("Authorization", "Basic %s" % base64string.decode('utf-8')) + connection = urllib.request.urlopen(request) except urllib.error.URLError as e: print('[prerun] Unable to connect to solr...try again in a while.') import time From dbcdd5e8130c4679710787c2be0fd265bfe219c8 Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Wed, 13 Mar 2024 13:57:40 +0100 Subject: [PATCH 200/213] new release ckan-2.9.11 & ckan-2.10.4 --- images/ckan/2.10/Dockerfile | 4 ++-- images/ckan/2.10/Dockerfile.focal | 4 ++-- images/ckan/2.9/Dockerfile | 4 ++-- images/ckan/2.9/Dockerfile.focal | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/images/ckan/2.10/Dockerfile b/images/ckan/2.10/Dockerfile index e7140ef..3b78ed6 100644 --- a/images/ckan/2.10/Dockerfile +++ b/images/ckan/2.10/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.17.2 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.10.3 +ENV IMAGE_TAG=2.10.4 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.10.3 +ENV GIT_BRANCH=ckan-2.10.4 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.10/Dockerfile.focal b/images/ckan/2.10/Dockerfile.focal index 6aa802c..84ec629 100644 --- a/images/ckan/2.10/Dockerfile.focal +++ b/images/ckan/2.10/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.10.3-focal +ENV IMAGE_TAG=2.10.4-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.10.3 +ENV GIT_BRANCH=ckan-2.10.4 # Set timezone ENV TZ=UTC diff --git a/images/ckan/2.9/Dockerfile b/images/ckan/2.9/Dockerfile index c1423d3..7c872d2 100644 --- a/images/ckan/2.9/Dockerfile +++ b/images/ckan/2.9/Dockerfile @@ -4,11 +4,11 @@ FROM alpine:3.13.7 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.10 +ENV IMAGE_TAG=2.9.11 # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.10 +ENV GIT_BRANCH=ckan-2.9.11 # Set src dirs ENV SRC_DIR=/srv/app/src diff --git a/images/ckan/2.9/Dockerfile.focal b/images/ckan/2.9/Dockerfile.focal index 336d7bc..a3047b8 100644 --- a/images/ckan/2.9/Dockerfile.focal +++ b/images/ckan/2.9/Dockerfile.focal @@ -4,11 +4,11 @@ FROM ubuntu:focal-20210827 as ckanbuild # Used by Github Actions to tag the image with -ENV IMAGE_TAG=2.9.10-focal +ENV IMAGE_TAG=2.9.11-focal # Set CKAN version to build ENV GIT_URL=https://github.com/ckan/ckan.git -ENV GIT_BRANCH=ckan-2.9.10 +ENV GIT_BRANCH=ckan-2.9.11 # Set timezone ENV TZ=UTC From e2ab81e7b7340570292f4e6b374bf4921e347838 Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Wed, 13 Mar 2024 14:12:46 +0100 Subject: [PATCH 201/213] change version in compose and in examples --- compose/2.10/docker-compose.yml | 2 +- compose/2.9/.ckan-env | 2 +- compose/2.9/.env | 2 +- compose/2.9/solr8/ckan_init_solr.sh | 2 +- examples/harvest/.ckan-env | 2 +- examples/harvest/.env | 2 +- examples/harvest/Dockerfile | 4 ++-- examples/s3filestore/.ckan-env | 2 +- examples/s3filestore/.env | 2 +- examples/s3filestore/Dockerfile | 4 ++-- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/compose/2.10/docker-compose.yml b/compose/2.10/docker-compose.yml index 00b40d2..b622f98 100644 --- a/compose/2.10/docker-compose.yml +++ b/compose/2.10/docker-compose.yml @@ -9,7 +9,7 @@ volumes: services: ckan: container_name: ckan - image: ghcr.io/keitaroinc/ckan:2.10.2 + image: ghcr.io/keitaroinc/ckan:2.10.4 networks: - frontend - backend diff --git a/compose/2.9/.ckan-env b/compose/2.9/.ckan-env index 98a0198..c264a1c 100644 --- a/compose/2.9/.ckan-env +++ b/compose/2.9/.ckan-env @@ -36,5 +36,5 @@ CKAN__DATAPUSHER__URL=http://datapusher:8000 CKAN__DATAPUSHER__CALLBACK_URL_BASE=http://ckan:5000/ # Solr configuration -CKAN_VERSION=2.9.9 +CKAN_VERSION=2.9.11 CKAN_CORE_NAME=ckan diff --git a/compose/2.9/.env b/compose/2.9/.env index d07bbb5..2eba8d0 100644 --- a/compose/2.9/.env +++ b/compose/2.9/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.9 +CKAN_VERSION=2.9.11 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/compose/2.9/solr8/ckan_init_solr.sh b/compose/2.9/solr8/ckan_init_solr.sh index 1e1339e..1f8d0c9 100755 --- a/compose/2.9/solr8/ckan_init_solr.sh +++ b/compose/2.9/solr8/ckan_init_solr.sh @@ -4,7 +4,7 @@ # Arguments are supplied via environment variables: CKAN_CORE_NAME CKAN_VERSION # Example: # CKAN_CORE_NAME=ckan -# CKAN_VERSION=2.9.9 +# CKAN_VERSION=2.9.11 set -e diff --git a/examples/harvest/.ckan-env b/examples/harvest/.ckan-env index e8ae538..5e19ef0 100644 --- a/examples/harvest/.ckan-env +++ b/examples/harvest/.ckan-env @@ -33,5 +33,5 @@ CKAN__HARVEST__MQ__TYPE=redis CKAN__HARVEST__MQ__HOSTNAME=redis # Solr configuration -CKAN_VERSION=2.9.9 +CKAN_VERSION=2.9.11 CKAN_CORE_NAME=ckan diff --git a/examples/harvest/.env b/examples/harvest/.env index d07bbb5..2eba8d0 100644 --- a/examples/harvest/.env +++ b/examples/harvest/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.9 +CKAN_VERSION=2.9.11 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/harvest/Dockerfile b/examples/harvest/Dockerfile index 2ff4b5d..302bd89 100644 --- a/examples/harvest/Dockerfile +++ b/examples/harvest/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.9 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.11 as extbuild # Locations and tags, please use specific tags or revisions ENV HARVEST_GIT_URL=https://github.com/ckan/ckanext-harvest @@ -30,7 +30,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.9 +FROM ghcr.io/keitaroinc/ckan:2.9.11 LABEL maintainer="Keitaro Inc " diff --git a/examples/s3filestore/.ckan-env b/examples/s3filestore/.ckan-env index 90148d1..b20ccf2 100644 --- a/examples/s3filestore/.ckan-env +++ b/examples/s3filestore/.ckan-env @@ -38,5 +38,5 @@ CKANEXT__S3FILESTORE__REGION_NAME=us-east-1 CKANEXT__S3FILESTORE__SIGNATURE_VERSION=s3v4 # Solr configuration -CKAN_VERSION=2.9.9 +CKAN_VERSION=2.9.11 CKAN_CORE_NAME=ckan diff --git a/examples/s3filestore/.env b/examples/s3filestore/.env index d07bbb5..2eba8d0 100644 --- a/examples/s3filestore/.env +++ b/examples/s3filestore/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.9.9 +CKAN_VERSION=2.9.11 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 diff --git a/examples/s3filestore/Dockerfile b/examples/s3filestore/Dockerfile index f6487dd..b4f05d0 100644 --- a/examples/s3filestore/Dockerfile +++ b/examples/s3filestore/Dockerfile @@ -1,7 +1,7 @@ ################### ### Extensions #### ################### -FROM ghcr.io/keitaroinc/ckan:2.9.9 as extbuild +FROM ghcr.io/keitaroinc/ckan:2.9.11 as extbuild # Locations and tags, please use specific tags or revisions ENV S3FILESTORE_GIT_URL=https://github.com/keitaroinc/ckanext-s3filestore @@ -20,7 +20,7 @@ USER ckan ############ ### MAIN ### ############ -FROM ghcr.io/keitaroinc/ckan:2.9.9 +FROM ghcr.io/keitaroinc/ckan:2.9.11 LABEL maintainer="Keitaro Inc " From f51a4801672c4e756ae4c1aa96bfe44c35b243db Mon Sep 17 00:00:00 2001 From: Kiril-Poposki1998 Date: Wed, 13 Mar 2024 14:25:56 +0100 Subject: [PATCH 202/213] add variable to docker-compose --- compose/2.10/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/2.10/docker-compose.yml b/compose/2.10/docker-compose.yml index b622f98..091a3e4 100644 --- a/compose/2.10/docker-compose.yml +++ b/compose/2.10/docker-compose.yml @@ -9,7 +9,7 @@ volumes: services: ckan: container_name: ckan - image: ghcr.io/keitaroinc/ckan:2.10.4 + image: ghcr.io/keitaroinc/ckan:${CKAN_VERSION} networks: - frontend - backend From 260c77752b82304288693ccdee5b2bdfb35ff7ca Mon Sep 17 00:00:00 2001 From: "stojanovskis1@icloud.com" Date: Wed, 13 Mar 2024 14:27:45 +0100 Subject: [PATCH 203/213] changed version in .env --- compose/2.10/.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/2.10/.env b/compose/2.10/.env index b9e1c2d..d8f5ef1 100644 --- a/compose/2.10/.env +++ b/compose/2.10/.env @@ -12,7 +12,7 @@ POSTGRES_PORT=5432 DATASTORE_READONLY_PASSWORD=datastore # CKAN -CKAN_VERSION=2.10 +CKAN_VERSION=2.10.4 CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 From f696c00a047a5561f8d72e354ceacc7cf1f180a5 Mon Sep 17 00:00:00 2001 From: Kiril-Poposki1998 Date: Wed, 13 Mar 2024 15:03:57 +0100 Subject: [PATCH 204/213] fix solr schema download --- compose/2.10/solr8/ckan_init_solr.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compose/2.10/solr8/ckan_init_solr.sh b/compose/2.10/solr8/ckan_init_solr.sh index 296d52b..091450f 100755 --- a/compose/2.10/solr8/ckan_init_solr.sh +++ b/compose/2.10/solr8/ckan_init_solr.sh @@ -8,7 +8,10 @@ set -e -CKAN_SOLR_SCHEMA_URL=https://raw.githubusercontent.com/ckan/ckan/$CKAN_VERSION/ckan/config/solr/schema.xml +MAJOR_VERSION=`echo $CKAN_VERSION | cut -d "." -f 1` +MINOR_VERSION=`echo $CKAN_VERSION | cut -d "." -f 2` + +CKAN_SOLR_SCHEMA_URL=https://raw.githubusercontent.com/ckan/ckan/`echo $MAJOR_VERSION.$MINOR_VERSION`/ckan/config/solr/schema.xml echo "Check whether managed schema exists for CKAN $CKAN_VERSION" if ! curl --output /dev/null --silent --head --fail "$CKAN_SOLR_SCHEMA_URL"; then From 0b07379bb3ac4b30fd9505f8e0ba7466fca73305 Mon Sep 17 00:00:00 2001 From: Maicol Battistini Date: Tue, 16 Jul 2024 16:12:55 +0200 Subject: [PATCH 205/213] feat: Changed to ubuntu docker images --- compose/2.10/{.ckan-env => .ckan.env} | 6 +- compose/2.10/Dockerfile | 39 + compose/2.10/docker-compose.yml | 11 +- .../docker-entrypoint.d/01_setup_spatial.sh | 14 + .../02_setup_extensions.sh | 42 + compose/2.10/requirements.txt | 3 + .../2.10/src/ckanext-d4science/.coveragerc | 5 + .../.github/workflows/test.yml | 48 + compose/2.10/src/ckanext-d4science/.gitignore | 42 + compose/2.10/src/ckanext-d4science/LICENSE | 661 + .../2.10/src/ckanext-d4science/MANIFEST.in | 5 + compose/2.10/src/ckanext-d4science/README.md | 123 + .../src/ckanext-d4science/ckanext/__init__.py | 9 + .../ckanext/d4science/__init__.py | 0 .../ckanext/d4science/assets/.gitignore | 0 .../ckanext/d4science/assets/script.js | 10 + .../ckanext/d4science/assets/style.css | 5 + .../ckanext/d4science/assets/webassets.yml | 14 + .../ckanext/d4science/cli.py | 20 + .../ckanext/d4science/helpers.py | 9 + .../ckanext/d4science/i18n/.gitignore | 0 .../ckanext/d4science/logic/action.py | 25 + .../ckanext/d4science/logic/auth.py | 12 + .../ckanext/d4science/logic/schema.py | 11 + .../ckanext/d4science/logic/validators.py | 13 + .../ckanext/d4science/plugin.py | 77 + .../ckanext/d4science/public/.gitignore | 0 .../ckanext/d4science/templates/.gitignore | 0 .../ckanext/d4science/tests/__init__.py | 0 .../d4science/tests/logic/test_action.py | 13 + .../d4science/tests/logic/test_auth.py | 19 + .../d4science/tests/logic/test_validators.py | 16 + .../ckanext/d4science/tests/test_helpers.py | 7 + .../ckanext/d4science/tests/test_plugin.py | 56 + .../ckanext/d4science/tests/test_views.py | 16 + .../ckanext/d4science/views.py | 17 + .../ckanext-d4science/dev-requirements.txt | 1 + .../src/ckanext-d4science/requirements.txt | 0 compose/2.10/src/ckanext-d4science/setup.cfg | 62 + compose/2.10/src/ckanext-d4science/setup.py | 16 + compose/2.10/src/ckanext-d4science/test.ini | 45 + .../2.10/src/ckanext-d4science_theme/.project | 17 + .../org.eclipse.core.resources.prefs | 2 + .../2.10/src/ckanext-d4science_theme/LICENSE | 661 + .../src/ckanext-d4science_theme/MANIFEST.in | 2 + .../src/ckanext-d4science_theme/README.rst | 129 + .../ckanext/__init__.py | 7 + .../ckanext/activity/__init__.py | 0 .../activity/assets/activity-stream.js | 34 + .../ckanext/activity/assets/activity.css | 103 + .../ckanext/activity/assets/activity.css.map | 7 + .../ckanext/activity/assets/activity.scss | 154 + .../ckanext/activity/assets/dashboard.js | 95 + .../ckanext/activity/assets/dashboard.spec.js | 64 + .../ckanext/activity/assets/webassets.yml | 14 + .../ckanext/activity/changes.py | 1255 + .../ckanext/activity/email_notifications.py | 278 + .../ckanext/activity/helpers.py | 190 + .../ckanext/activity/logic/__init__.py | 0 .../ckanext/activity/logic/action.py | 647 + .../ckanext/activity/logic/auth.py | 184 + .../ckanext/activity/logic/schema.py | 102 + .../ckanext/activity/logic/validators.py | 97 + .../ckanext/activity/model/__init__.py | 28 + .../ckanext/activity/model/activity.py | 791 + .../ckanext/activity/plugin.py | 30 + .../ckanext/activity/public/dotted.png | Bin 0 -> 74 bytes .../ckanext/activity/subscriptions.py | 177 + .../activity_stream_email_notifications.text | 7 + .../templates/ajax_snippets/dashboard.html | 4 + .../ckanext/activity/templates/base.html | 6 + .../templates/group/activity_stream.html | 15 + .../activity/templates/group/changes.html | 64 + .../activity/templates/group/read_base.html | 6 + .../templates/group/snippets/item_group.html | 10 + .../ckanext/activity/templates/header.html | 15 + .../organization/activity_stream.html | 14 + .../templates/organization/changes.html | 64 + .../templates/organization/read_base.html | 6 + .../snippets/item_organization.html | 10 + .../templates/package/activity_stream.html | 24 + .../activity/templates/package/changes.html | 64 + .../activity/templates/package/history.html | 23 + .../activity/templates/package/read_base.html | 6 + .../templates/package/resource_history.html | 26 + .../package/snippets/change_item.html | 10 + .../package/snippets/resource_item.html | 16 + .../templates/package/snippets/resources.html | 12 + .../package/snippets/resources_list.html | 15 + .../ckanext/activity/templates/page.html | 6 + .../snippets/activities/added_tag.html | 15 + .../snippets/activities/changed_group.html | 20 + .../activities/changed_organization.html | 20 + .../snippets/activities/changed_package.html | 26 + .../snippets/activities/changed_resource.html | 15 + .../snippets/activities/changed_user.html | 13 + .../snippets/activities/deleted_group.html | 14 + .../activities/deleted_organization.html | 14 + .../snippets/activities/deleted_package.html | 17 + .../snippets/activities/deleted_resource.html | 15 + .../snippets/activities/fallback.html | 38 + .../snippets/activities/follow_dataset.html | 14 + .../snippets/activities/follow_group.html | 14 + .../snippets/activities/follow_user.html | 14 + .../snippets/activities/new_group.html | 14 + .../snippets/activities/new_organization.html | 14 + .../snippets/activities/new_package.html | 22 + .../snippets/activities/new_resource.html | 15 + .../snippets/activities/new_user.html | 13 + .../snippets/activities/removed_tag.html | 15 + .../snippets/activity_type_selector.html | 25 + .../templates/snippets/changes/author.html | 30 + .../snippets/changes/author_email.html | 47 + .../snippets/changes/delete_resource.html | 10 + .../snippets/changes/extension_fields.html | 9 + .../snippets/changes/extra_fields.html | 85 + .../templates/snippets/changes/license.html | 67 + .../snippets/changes/maintainer.html | 30 + .../snippets/changes/maintainer_email.html | 47 + .../templates/snippets/changes/name.html | 23 + .../templates/snippets/changes/new_file.html | 10 + .../snippets/changes/new_resource.html | 17 + .../templates/snippets/changes/no_change.html | 5 + .../templates/snippets/changes/notes.html | 30 + .../templates/snippets/changes/org.html | 30 + .../templates/snippets/changes/private.html | 8 + .../snippets/changes/resource_desc.html | 39 + .../snippets/changes/resource_extras.html | 112 + .../snippets/changes/resource_format.html | 54 + .../snippets/changes/resource_name.html | 13 + .../templates/snippets/changes/tags.html | 59 + .../templates/snippets/changes/title.html | 8 + .../templates/snippets/changes/url.html | 46 + .../templates/snippets/changes/version.html | 30 + .../snippets/group_changes/description.html | 30 + .../snippets/group_changes/image_url.html | 46 + .../snippets/group_changes/no_change.html | 5 + .../snippets/group_changes/title.html | 8 + .../organization_changes/description.html | 30 + .../organization_changes/image_url.html | 46 + .../organization_changes/no_change.html | 5 + .../snippets/organization_changes/title.html | 8 + .../templates/snippets/pagination.html | 7 + .../activity/templates/snippets/stream.html | 63 + .../templates/user/activity_stream.html | 14 + .../activity/templates/user/dashboard.html | 27 + .../templates/user/edit_user_form.html | 12 + .../activity/templates/user/read_base.html | 6 + .../user/snippets/followee_dropdown.html | 52 + .../ckanext/activity/tests/__init__.py | 0 .../ckanext/activity/tests/conftest.py | 15 + .../ckanext/activity/tests/logic/__init__.py | 0 .../activity/tests/logic/test_action.py | 2178 ++ .../ckanext/activity/tests/logic/test_auth.py | 46 + .../activity/tests/logic/test_functional.py | 40 + .../activity/tests/logic/test_pagination.py | 201 + .../ckanext/activity/tests/model/__init__.py | 0 .../activity/tests/model/test_activity.py | 56 + .../ckanext/activity/tests/test_changes.py | 1229 + .../tests/test_email_notifications.py | 66 + .../ckanext/activity/tests/test_helpers.py | 55 + .../ckanext/activity/tests/test_views.py | 958 + .../ckanext/activity/views.py | 943 + .../ckanext/audioview/__init__.py | 0 .../ckanext/audioview/plugin.py | 50 + .../ckanext/audioview/tests/__init__.py | 0 .../ckanext/audioview/tests/test_view.py | 28 + .../audioview/theme/templates/audio_form.html | 3 + .../audioview/theme/templates/audio_view.html | 9 + .../ckanext/chained_functions/__init__.py | 0 .../ckanext/chained_functions/plugin.py | 55 + .../chained_functions/tests/__init__.py | 0 .../chained_functions/tests/test_plugin.py | 22 + .../ckanext/d4science_theme/__init__.py | 19 + .../assets/css/d4science_theme.css | 844 + .../assets/js/d4science_scripts.js | 305 + .../d4science_theme/assets/webassets.yml | 15 + .../d4science_theme/controllers/__init__.py | 4 + .../d4science_theme/controllers/home.py | 82 + .../controllers/organization.py | 134 + .../d4science_theme/controllers/systemtype.py | 95 + .../d4science_theme/d4sdiscovery/__init__.py | 0 .../d4sdiscovery/d4s_cache_controller.py | 106 + .../d4sdiscovery/d4s_extras.py | 30 + .../d4sdiscovery/d4s_namespaces.py | 39 + .../d4sdiscovery/d4s_namespaces_controller.py | 130 + .../d4s_namespaces_extras_util.py | 89 + .../d4sdiscovery/icproxycontroller.py | 110 + .../d4science_theme/fanstatic/.gitignore | 0 .../fanstatic/d4science_scripts.js | 303 + .../fanstatic/d4science_theme.css | 844 + .../ckanext/d4science_theme/helpers.py | 719 + .../ckanext/d4science_theme/plugin.py | 454 + .../ckanext/d4science_theme/public/.gitignore | 0 .../public/D4ScienceDataCataloguelogo.png | Bin 0 -> 8358 bytes .../public/D4ScienceDataCataloguelogo2.png | Bin 0 -> 115278 bytes .../d4science_theme/public/bg-noise.png | Bin 0 -> 1829 bytes .../d4science_theme/public/bg-pattern.svg | 1 + .../public/ckan-logo-footer.png | Bin 0 -> 1684 bytes .../public/d4ScienceDataCatalogue.png | Bin 0 -> 117659 bytes .../d4science_theme/public/d4s_tagcloud.js | 58 + .../d4science_theme/public/d4science.ico | Bin 0 -> 1150 bytes .../d4science_theme/public/d4science_logo.png | Bin 0 -> 31965 bytes .../d4science_theme/public/favicon.ico | Bin 0 -> 1150 bytes .../d4science_theme/public/gCube_70.png | Bin 0 -> 8275 bytes .../public/gcubedatacataloguelogo.png | Bin 0 -> 10016 bytes .../public/grsf/GRSF_for_admins_logo.png | Bin 0 -> 95418 bytes .../images/groups/icon/placeholder-group.png | Bin 0 -> 1218 bytes .../images/organisations/d4sciencelabs.png | Bin 0 -> 9879 bytes .../public/images/organisations/emodnet.png | Bin 0 -> 30358 bytes .../public/images/organisations/fao.png | Bin 0 -> 5365 bytes .../public/images/organisations/grsf.png | Bin 0 -> 97630 bytes .../organisations/icon/d4sciencelabs.png | Bin 0 -> 8550 bytes .../images/organisations/icon/emodnet.png | Bin 0 -> 10980 bytes .../public/images/organisations/icon/fao.png | Bin 0 -> 5509 bytes .../public/images/organisations/icon/grsf.png | Bin 0 -> 10980 bytes .../images/organisations/icon/imarine.png | Bin 0 -> 58152 bytes .../icon/placeholder-organization.png | Bin 0 -> 2969 bytes .../organisations/icon/rprototypinglab.png | Bin 0 -> 22567 bytes .../public/images/organisations/imarine.png | Bin 0 -> 106027 bytes .../images/organisations/rprototypinglab.png | Bin 0 -> 8193 bytes .../public/images/types/icon/codelist.png | Bin 0 -> 2341 bytes .../public/images/types/icon/dataset.png | Bin 0 -> 2308 bytes .../public/images/types/icon/dsd.png | Bin 0 -> 2325 bytes .../public/images/types/icon/metadata.png | Bin 0 -> 2188 bytes .../images/types/icon/placeholder-type.png | Bin 0 -> 3349 bytes .../images/types/icon/researchobject.png | Bin 0 -> 2625 bytes .../public/images/types/icon/series.png | Bin 0 -> 1723 bytes .../public/images/types/icon/service.png | Bin 0 -> 3593 bytes .../images/types/icon/trainingmaterial.png | Bin 0 -> 3612 bytes .../types/icon/virtualresearchenvironment.png | Bin 0 -> 2608 bytes .../public/images/types/placeholder-type.png | Bin 0 -> 107945 bytes .../d4science_theme/public/jquery.tagcloud.js | 92 + .../public/location_to_bboxes.ini | 16 + .../d4science_theme/public/oai2_style.xsl | 690 + .../d4science_theme/public/pageloading.gif | Bin 0 -> 100082 bytes .../public/parthenos/logo-parthenos.png | Bin 0 -> 28541 bytes .../public/placeholder_types.png | Bin 0 -> 3151 bytes .../d4science_theme/qrcodelink/__init__.py | 0 .../qrcodelink/generate_qrcode.py | 80 + .../activity_stream_email_notifications.text | 7 + .../d4science_theme/templates/admin/base.html | 12 + .../templates/admin/config.html | 72 + .../templates/admin/confirm_reset.html | 14 + .../templates/admin/index.html | 84 + .../admin/snippets/confirm_delete.html | 24 + .../templates/admin/snippets/data_type.html | 57 + .../templates/admin/trash.html | 38 + .../ajax_snippets/custom_fields.html | 4 + .../ajax_snippets/follow_button.html | 1 + .../d4science_theme/templates/base.html | 130 + .../templates/dataviewer/base.html | 15 + .../templates/development/primer.html | 100 + .../development/snippets/actions.html | 2 + .../development/snippets/breadcrumb.html | 7 + .../development/snippets/context.html | 26 + .../templates/development/snippets/facet.html | 15 + .../templates/development/snippets/form.html | 27 + .../development/snippets/form_stages.html | 30 + .../templates/development/snippets/list.html | 14 + .../development/snippets/media_grid.html | 5 + .../development/snippets/module.html | 21 + .../templates/development/snippets/nav.html | 14 + .../development/snippets/page_header.html | 11 + .../development/snippets/pagination.html | 11 + .../development/snippets/simple-input.html | 4 + .../templates/emails/invite_user.txt | 19 + .../templates/emails/invite_user_subject.txt | 3 + .../templates/emails/reset_password.txt | 14 + .../emails/reset_password_subject.txt | 3 + .../templates/error_document_template.html | 31 + .../d4science_theme/templates/footer.html | 20 + .../templates/group/about.html | 16 + .../templates/group/admins.html | 10 + .../templates/group/base_form_page.html | 13 + .../templates/group/confirm_delete.html | 22 + .../group/confirm_delete_member.html | 23 + .../d4science_theme/templates/group/edit.html | 12 + .../templates/group/edit_base.html | 19 + .../templates/group/followers.html | 10 + .../templates/group/index.html | 44 + .../templates/group/member_new.html | 101 + .../templates/group/members.html | 38 + .../d4science_theme/templates/group/new.html | 25 + .../templates/group/new_group_form.html | 25 + .../d4science_theme/templates/group/read.html | 42 + .../templates/group/read_base.html | 29 + .../templates/group/snippets/feeds.html | 2 + .../templates/group/snippets/group_form.html | 45 + .../templates/group/snippets/group_item.html | 50 + .../templates/group/snippets/group_list.html | 19 + .../templates/group/snippets/helper.html | 27 + .../templates/group/snippets/info.html | 54 + .../d4science_theme/templates/header.html | 123 + .../d4science_theme/templates/home/about.html | 24 + .../d4science_theme/templates/home/index.html | 18 + .../templates/home/layout1.html | 40 + .../templates/home/layout2.html | 35 + .../templates/home/layout3.html | 23 + .../d4science_theme/templates/home/robots.txt | 12 + .../templates/home/snippets/about_text.html | 20 + .../home/snippets/featured_group.html | 7 + .../home/snippets/featured_organization.html | 7 + .../templates/home/snippets/promoted.html | 27 + .../templates/home/snippets/search.html | 22 + .../templates/home/snippets/stats.html | 29 + .../templates/macros/autoform.html | 70 + .../templates/macros/form.html | 36 + .../templates/macros/form/attributes.html | 17 + .../templates/macros/form/checkbox.html | 34 + .../templates/macros/form/custom.html | 61 + .../templates/macros/form/errors.html | 26 + .../templates/macros/form/hidden.html | 14 + .../macros/form/hidden_from_list.html | 28 + .../templates/macros/form/image_upload.html | 59 + .../templates/macros/form/info.html | 24 + .../templates/macros/form/input.html | 30 + .../templates/macros/form/input_block.html | 32 + .../templates/macros/form/markdown.html | 33 + .../templates/macros/form/prepend.html | 38 + .../macros/form/required_message.html | 13 + .../templates/macros/form/select.html | 50 + .../templates/macros/form/textarea.html | 31 + .../templates/organization/about.html | 15 + .../templates/organization/admins.html | 10 + .../organization/base_form_page.html | 10 + .../templates/organization/bulk_process.html | 114 + .../organization/confirm_delete.html | 22 + .../organization/confirm_delete_member.html | 23 + .../templates/organization/edit.html | 14 + .../templates/organization/edit_base.html | 26 + .../templates/organization/index.html | 43 + .../templates/organization/member_new.html | 106 + .../templates/organization/members.html | 43 + .../templates/organization/new.html | 19 + .../organization/new_organization_form.html | 27 + .../templates/organization/read.html | 47 + .../templates/organization/read_base.html | 30 + .../organization/snippets/feeds.html | 2 + .../organization/snippets/helper.html | 27 + .../snippets/organization_form.html | 45 + .../snippets/organization_item.html | 51 + .../snippets/organization_list.html | 19 + .../templates/organization_vre/index.html | 47 + .../templates/organization_vre/read.html | 61 + .../templates/package/base.html | 26 + .../templates/package/base_form_page.html | 36 + .../collaborators/collaborator_new.html | 77 + .../package/collaborators/collaborators.html | 43 + .../package/collaborators/confirm_delete.html | 22 + .../templates/package/confirm_delete.html | 23 + .../package/confirm_delete_resource.html | 22 + .../templates/package/edit.html | 7 + .../templates/package/edit_base.html | 26 + .../templates/package/edit_view.html | 25 + .../templates/package/followers.html | 10 + .../templates/package/group_list.html | 30 + .../templates/package/new.html | 10 + .../templates/package/new_package_form.html | 27 + .../templates/package/new_resource.html | 24 + .../package/new_resource_not_draft.html | 21 + .../templates/package/new_view.html | 33 + .../templates/package/read.html | 46 + .../templates/package/read_base.html | 48 + .../templates/package/resource_edit.html | 14 + .../templates/package/resource_edit_base.html | 41 + .../templates/package/resource_read.html | 218 + .../templates/package/resource_views.html | 29 + .../templates/package/resources.html | 31 + .../templates/package/search.html | 90 + .../package/snippets/additional_info.html | 95 + .../snippets/cannot_create_package.html | 27 + .../templates/package/snippets/info.html | 41 + .../snippets/new_package_breadcrumb.html | 2 + .../snippets/package_basic_fields.html | 123 + .../package/snippets/package_form.html | 49 + .../snippets/package_metadata_fields.html | 34 + .../package/snippets/resource_edit_form.html | 12 + .../package/snippets/resource_form.html | 95 + .../package/snippets/resource_help.html | 6 + .../package/snippets/resource_info.html | 25 + .../package/snippets/resource_item.html | 82 + .../snippets/resource_upload_field.html | 114 + .../package/snippets/resource_view.html | 92 + .../package/snippets/resource_view_embed.html | 9 + .../snippets/resource_view_filters.html | 8 + .../package/snippets/resource_views_list.html | 15 + .../snippets/resource_views_list_item.html | 19 + .../templates/package/snippets/resources.html | 34 + .../package/snippets/resources_list.html | 37 + .../templates/package/snippets/stages.html | 42 + .../templates/package/snippets/tags.html | 15 + .../templates/package/snippets/view_form.html | 19 + .../package/snippets/view_form_filters.html | 48 + .../templates/package/snippets/view_help.html | 6 + .../templates/package/view_edit_base.html | 55 + .../d4science_theme/templates/page.html | 137 + .../templates/revision/__init__.py | 3 + .../templates/snippets/add_dataset.html | 11 + .../templates/snippets/additional_info.html | 25 + .../templates/snippets/context.html | 10 + .../templates/snippets/context/dataset.html | 21 + .../templates/snippets/context/group.html | 23 + .../templates/snippets/context/user.html | 25 + .../templates/snippets/csrf_input.html | 2 + .../snippets/custom_form_fields.html | 42 + .../templates/snippets/datapusher_status.html | 14 + .../templates/snippets/disqus_trackback.html | 4 + .../templates/snippets/facet_list.html | 100 + .../templates/snippets/follow_button.html | 11 + .../templates/snippets/group.html | 25 + .../templates/snippets/group_item.html | 27 + .../snippets/home_breadcrumb_item.html | 2 + .../templates/snippets/language_selector.html | 17 + .../templates/snippets/license.html | 37 + .../snippets/local_friendly_datetime.html | 14 + .../templates/snippets/organization.html | 73 + .../templates/snippets/organization_item.html | 27 + .../templates/snippets/package_item.html | 70 + .../templates/snippets/package_list.html | 24 + .../templates/snippets/popular.html | 4 + .../templates/snippets/private.html | 3 + .../templates/snippets/search_form.html | 90 + .../snippets/search_result_text.html | 58 + .../templates/snippets/simple_search.html | 17 + .../templates/snippets/social.html | 14 + .../templates/snippets/tag_list.html | 20 + .../tests/broken_helper_as_attribute.html | 5 + .../tests/broken_helper_as_item.html | 5 + .../templates/tests/flash_messages.html | 14 + .../templates/tests/helper_as_attribute.html | 5 + .../templates/tests/helper_as_item.html | 5 + .../mock_json_resource_preview_template.html | 17 + .../tests/mock_resource_preview_template.html | 17 + .../templates/user/api_tokens.html | 42 + .../templates/user/confirm_delete.html | 22 + .../templates/user/dashboard.html | 43 + .../templates/user/dashboard_datasets.html | 22 + .../templates/user/dashboard_groups.html | 26 + .../user/dashboard_organizations.html | 27 + .../d4science_theme/templates/user/edit.html | 25 + .../templates/user/edit_base.html | 11 + .../templates/user/edit_user_form.html | 96 + .../templates/user/followers.html | 12 + .../d4science_theme/templates/user/list.html | 33 + .../d4science_theme/templates/user/login.html | 54 + .../templates/user/logout.html | 14 + .../templates/user/logout_first.html | 26 + .../d4science_theme/templates/user/new.html | 33 + .../templates/user/new_user_form.html | 41 + .../templates/user/perform_reset.html | 51 + .../d4science_theme/templates/user/read.html | 32 + .../templates/user/read_base.html | 110 + .../templates/user/request_reset.html | 46 + .../user/snippets/api_token_list.html | 52 + .../templates/user/snippets/followers.html | 10 + .../templates/user/snippets/login_form.html | 32 + .../templates/user/snippets/placeholder.html | 2 + .../templates/user/snippets/recaptcha.html | 18 + .../templates/user/snippets/user_search.html | 13 + .../d4science_theme/templates2.6/.gitignore | 0 .../d4science_theme/templates2.6/base.html | 121 + .../d4science_theme/templates2.6/footer.html | 20 + .../templates2.6/group/read_base.html | 36 + .../group/snippets/group_form.html | 108 + .../group/snippets/group_item.html | 65 + .../group/snippets/group_list.html | 56 + .../group/snippets/group_list_simple.html | 20 + .../group/snippets/group_tree.html | 32 + .../d4science_theme/templates2.6/header.html | 145 + .../templates2.6/home/index.html | 1 + .../templates2.6/home/layout2.html | 108 + .../home/snippets/featured_organization.html | 7 + .../home/snippets/popular_formats.html | 27 + .../home/snippets/popular_groups.html | 25 + .../home/snippets/popular_metadatatypes.html | 31 + .../home/snippets/popular_tags.html | 30 + .../templates2.6/home/snippets/promoted.html | 27 + .../templates2.6/home/snippets/search.html | 26 + .../home/snippets/search_for_groups.html | 35 + .../home/snippets/search_for_location.html | 44 + .../snippets/search_for_organisations.html | 33 + .../home/snippets/search_for_types.html | 37 + .../templates2.6/home/snippets/stats.html | 43 + .../templates2.6/organization/read.html | 47 + .../templates2.6/organization/read_base.html | 34 + .../snippets/organization_item.html | 44 + .../templates2.6/organization_vre/index.html | 47 + .../templates2.6/organization_vre/read.html | 61 + .../templates2.6/package/base.html | 24 + .../templates2.6/package/group_list.html | 44 + .../templates2.6/package/read.html | 59 + .../templates2.6/package/read_base.html | 116 + .../package/resource_edit_base.html | 51 + .../templates2.6/package/resource_read.html | 187 + .../templates2.6/package/search.html | 90 + .../package/snippets/additional_info.html | 150 + .../package/snippets/extras_table.html | 59 + .../templates2.6/package/snippets/info.html | 47 + .../package/snippets/qrcode_show.html | 6 + .../package/snippets/resource_form.html | 96 + .../package/snippets/resource_item.html | 106 + .../package/snippets/resources.html | 34 + .../package/snippets/resources_list.html | 76 + .../d4science_theme/templates2.6/page.html | 134 + .../snippets/custom_form_fields.html | 51 + .../templates2.6/snippets/facet_list.html | 99 + .../templates2.6/snippets/package_item.html | 133 + .../templates2.6/snippets/tag_list.html | 18 + .../templates2.6/type/index.html | 41 + .../templates2.6/type/snippets/helper.html | 14 + .../templates2.6/type/snippets/type_form.html | 52 + .../templates2.6/type/snippets/type_item.html | 39 + .../templates2.6/type/snippets/type_list.html | 19 + .../templates2.6/user/dashboard.html | 54 + .../templates2.6/user/dashboard_datasets.html | 65 + .../templates2.6/user/dashboard_groups.html | 65 + .../user/dashboard_organizations.html | 66 + .../ckanext/d4science_theme/tests/__init__.py | 0 .../d4science_theme/tests/test_plugin.py | 5 + .../ckanext/datapusher/__init__.py | 0 .../ckanext/datapusher/assets/datapusher.css | 33 + .../datapusher/assets/datapusher.css.map | 7 + .../ckanext/datapusher/assets/datapusher.scss | 49 + .../datapusher/assets/datapusher_popover.js | 27 + .../ckanext/datapusher/assets/webassets.yml | 13 + .../ckanext/datapusher/cli.py | 105 + .../datapusher/config_declaration.yaml | 51 + .../ckanext/datapusher/helpers.py | 31 + .../ckanext/datapusher/interfaces.py | 55 + .../ckanext/datapusher/logic/__init__.py | 0 .../ckanext/datapusher/logic/action.py | 338 + .../ckanext/datapusher/logic/auth.py | 16 + .../ckanext/datapusher/logic/schema.py | 27 + .../ckanext/datapusher/plugin.py | 163 + .../ckanext/datapusher/public/dotted.png | Bin 0 -> 74 bytes .../datapusher/resource_data.html | 88 + .../package/resource_edit_base.html | 16 + .../templates/datapusher/resource_data.html | 95 + .../templates/package/resource_edit_base.html | 16 + .../ckanext/datapusher/tests/__init__.py | 6 + .../ckanext/datapusher/tests/test.py | 322 + .../ckanext/datapusher/tests/test_action.py | 347 + .../datapusher/tests/test_default_views.py | 102 + .../datapusher/tests/test_interfaces.py | 121 + .../ckanext/datapusher/tests/test_views.py | 38 + .../ckanext/datapusher/views.py | 75 + .../ckanext/datastore/__init__.py | 0 .../ckanext/datastore/allowed_functions.txt | 283 + .../ckanext/datastore/backend/__init__.py | 238 + .../ckanext/datastore/backend/postgres.py | 2323 ++ .../ckanext/datastore/blueprint.py | 290 + .../ckanext/datastore/cli.py | 179 + .../ckanext/datastore/config_declaration.yaml | 101 + .../ckanext/datastore/helpers.py | 235 + .../ckanext/datastore/interfaces.py | 180 + .../ckanext/datastore/logic/__init__.py | 0 .../ckanext/datastore/logic/action.py | 724 + .../ckanext/datastore/logic/auth.py | 95 + .../ckanext/datastore/logic/schema.py | 225 + .../ckanext/datastore/plugin.py | 279 + .../ckanext/datastore/set_permissions.sql | 108 + .../templates-bs3/ajax_snippets/api_info.html | 137 + .../datastore/api_examples/curl.html | 40 + .../datastore/api_examples/javascript.html | 55 + .../datastore/api_examples/powershell.html | 48 + .../datastore/api_examples/python.html | 52 + .../datastore/api_examples/r.html | 51 + .../templates-bs3/datastore/dictionary.html | 22 + .../datastore/snippets/dictionary_form.html | 25 + .../package/resource_edit_base.html | 9 + .../templates-bs3/package/resource_read.html | 49 + .../package/snippets/data_api_button.html | 10 + .../package/snippets/dictionary_table.html | 7 + .../package/snippets/resource_item.html | 8 + .../package/snippets/resources.html | 8 + .../templates/ajax_snippets/api_info.html | 137 + .../templates/datastore/dictionary.html | 22 + .../datastore/snippets/dictionary_form.html | 25 + .../templates/package/resource_edit_base.html | 8 + .../templates/package/resource_read.html | 37 + .../package/snippets/data_api_button.html | 10 + .../package/snippets/dictionary_table.html | 7 + .../ckanext/datastore/tests/__init__.py | 0 .../datastore/tests/allowed_functions.txt | 2 + .../ckanext/datastore/tests/conftest.py | 14 + .../ckanext/datastore/tests/helpers.py | 107 + .../tests/sample_datastore_plugin.py | 56 + .../ckanext/datastore/tests/test_auth.py | 278 + .../datastore/tests/test_chained_action.py | 98 + .../tests/test_chained_auth_functions.py | 100 + .../ckanext/datastore/tests/test_create.py | 1449 + .../ckanext/datastore/tests/test_db.py | 225 + .../ckanext/datastore/tests/test_delete.py | 447 + .../datastore/tests/test_dictionary.py | 35 + .../ckanext/datastore/tests/test_disable.py | 28 + .../ckanext/datastore/tests/test_dump.py | 669 + .../ckanext/datastore/tests/test_helpers.py | 214 + .../ckanext/datastore/tests/test_info.py | 88 + .../ckanext/datastore/tests/test_interface.py | 169 + .../ckanext/datastore/tests/test_plugin.py | 195 + .../ckanext/datastore/tests/test_search.py | 2181 ++ .../ckanext/datastore/tests/test_unit.py | 39 + .../ckanext/datastore/tests/test_upsert.py | 953 + .../ckanext/datastore/writer.py | 179 + .../ckanext/datatablesview/__init__.py | 0 .../ckanext/datatablesview/blueprint.py | 215 + .../datatablesview/config_declaration.yaml | 95 + .../ckanext/datatablesview/plugin.py | 99 + .../datatablesview/public/datatables_view.css | 220 + .../datatablesview/public/datatablesview.js | 944 + .../datatablesview/public/resource.config | 3 + .../DataTables/dataTables.scrollResize.js | 195 + .../public/vendor/DataTables/datatables.css | 1165 + .../public/vendor/DataTables/datatables.js | 25227 ++++++++++++++++ .../vendor/DataTables/datatables.mark.css | 4 + .../vendor/DataTables/datatables.mark.js | 119 + .../public/vendor/DataTables/datetime.js | 126 + .../public/vendor/DataTables/i18n/af.json | 22 + .../public/vendor/DataTables/i18n/am.json | 22 + .../public/vendor/DataTables/i18n/ar.json | 43 + .../public/vendor/DataTables/i18n/az.json | 22 + .../public/vendor/DataTables/i18n/az_az.json | 22 + .../public/vendor/DataTables/i18n/be.json | 19 + .../public/vendor/DataTables/i18n/bg.json | 15 + .../public/vendor/DataTables/i18n/bn.json | 15 + .../public/vendor/DataTables/i18n/bs.json | 76 + .../public/vendor/DataTables/i18n/ca.json | 44 + .../public/vendor/DataTables/i18n/cs.json | 22 + .../public/vendor/DataTables/i18n/cs_CZ.json | 134 + .../public/vendor/DataTables/i18n/cy.json | 22 + .../public/vendor/DataTables/i18n/da.json | 15 + .../public/vendor/DataTables/i18n/de.json | 43 + .../public/vendor/DataTables/i18n/el.json | 25 + .../public/vendor/DataTables/i18n/en-gb.json | 137 + .../public/vendor/DataTables/i18n/en_GB.json | 178 + .../public/vendor/DataTables/i18n/eo.json | 22 + .../public/vendor/DataTables/i18n/es.json | 26 + .../public/vendor/DataTables/i18n/et.json | 15 + .../public/vendor/DataTables/i18n/eu.json | 22 + .../public/vendor/DataTables/i18n/fa.json | 22 + .../public/vendor/DataTables/i18n/fi.json | 38 + .../public/vendor/DataTables/i18n/fil.json | 15 + .../public/vendor/DataTables/i18n/fr.json | 29 + .../public/vendor/DataTables/i18n/ga.json | 15 + .../public/vendor/DataTables/i18n/gl.json | 22 + .../public/vendor/DataTables/i18n/gu.json | 22 + .../public/vendor/DataTables/i18n/he.json | 16 + .../public/vendor/DataTables/i18n/hi.json | 15 + .../public/vendor/DataTables/i18n/hr.json | 22 + .../public/vendor/DataTables/i18n/hu.json | 38 + .../public/vendor/DataTables/i18n/hy.json | 22 + .../public/vendor/DataTables/i18n/id.json | 82 + .../public/vendor/DataTables/i18n/id_alt.json | 15 + .../public/vendor/DataTables/i18n/is.json | 22 + .../public/vendor/DataTables/i18n/it.json | 22 + .../public/vendor/DataTables/i18n/ja.json | 22 + .../public/vendor/DataTables/i18n/ka.json | 22 + .../public/vendor/DataTables/i18n/kk.json | 21 + .../public/vendor/DataTables/i18n/km.json | 22 + .../public/vendor/DataTables/i18n/kn.json | 22 + .../public/vendor/DataTables/i18n/ko.json | 22 + .../public/vendor/DataTables/i18n/ko_KR.json | 68 + .../public/vendor/DataTables/i18n/ku.json | 21 + .../public/vendor/DataTables/i18n/ky.json | 22 + .../public/vendor/DataTables/i18n/lo.json | 22 + .../public/vendor/DataTables/i18n/lt.json | 19 + .../public/vendor/DataTables/i18n/lv.json | 21 + .../public/vendor/DataTables/i18n/mk.json | 17 + .../public/vendor/DataTables/i18n/mn.json | 22 + .../public/vendor/DataTables/i18n/ms.json | 22 + .../public/vendor/DataTables/i18n/ne.json | 22 + .../public/vendor/DataTables/i18n/nl.json | 22 + .../public/vendor/DataTables/i18n/no.json | 22 + .../public/vendor/DataTables/i18n/no_nb.json | 22 + .../public/vendor/DataTables/i18n/pa.json | 22 + .../public/vendor/DataTables/i18n/pl.json | 21 + .../public/vendor/DataTables/i18n/ps.json | 22 + .../public/vendor/DataTables/i18n/pt.json | 37 + .../public/vendor/DataTables/i18n/pt_BR.json | 131 + .../public/vendor/DataTables/i18n/pt_pt.json | 21 + .../public/vendor/DataTables/i18n/rm.json | 43 + .../public/vendor/DataTables/i18n/ro.json | 15 + .../public/vendor/DataTables/i18n/ru.json | 28 + .../public/vendor/DataTables/i18n/si.json | 22 + .../public/vendor/DataTables/i18n/sk.json | 22 + .../public/vendor/DataTables/i18n/sl.json | 22 + .../public/vendor/DataTables/i18n/snd.json | 30 + .../public/vendor/DataTables/i18n/sq.json | 22 + .../public/vendor/DataTables/i18n/sr.json | 22 + .../vendor/DataTables/i18n/sr@latin.json | 22 + .../public/vendor/DataTables/i18n/sr_sp.json | 22 + .../public/vendor/DataTables/i18n/sv.json | 22 + .../public/vendor/DataTables/i18n/sw.json | 22 + .../public/vendor/DataTables/i18n/ta.json | 22 + .../public/vendor/DataTables/i18n/te.json | 22 + .../public/vendor/DataTables/i18n/tg.json | 28 + .../public/vendor/DataTables/i18n/th.json | 22 + .../public/vendor/DataTables/i18n/tl.json | 15 + .../public/vendor/DataTables/i18n/tr.json | 29 + .../public/vendor/DataTables/i18n/uk.json | 19 + .../public/vendor/DataTables/i18n/ur.json | 15 + .../public/vendor/DataTables/i18n/uz.json | 21 + .../public/vendor/DataTables/i18n/vi.json | 15 + .../public/vendor/DataTables/i18n/zh.json | 22 + .../public/vendor/DataTables/i18n/zh_CN.json | 22 + .../vendor/DataTables/i18n/zh_Hant.json | 20 + .../public/vendor/DataTables/jquery.mark.js | 1228 + .../FontAwesome/images/times-circle-solid.svg | 1 + .../datatablesview/public/webassets.yml | 18 + .../templates/datatables/datatables_form.html | 68 + .../templates/datatables/datatables_view.html | 103 + .../ckanext/expire_api_token/__init__.py | 0 .../ckanext/expire_api_token/plugin.py | 74 + .../templates/user/api_tokens.html | 18 + .../user/snippets/api_token_list.html | 16 + .../expire_api_token/tests/__init__.py | 0 .../expire_api_token/tests/test_plugin.py | 45 + .../ckanext/imageview/__init__.py | 0 .../ckanext/imageview/config_declaration.yaml | 8 + .../ckanext/imageview/plugin.py | 45 + .../ckanext/imageview/tests/__init__.py | 0 .../ckanext/imageview/tests/test_view.py | 28 + .../imageview/theme/templates/image_form.html | 3 + .../imageview/theme/templates/image_view.html | 1 + .../ckanext/multilingual/__init__.py | 0 .../ckanext/multilingual/plugin.py | 413 + .../ckanext/multilingual/solr/dutch_stop.txt | 117 + .../multilingual/solr/english_stop.txt | 317 + .../ckanext/multilingual/solr/fr_elision.txt | 8 + .../ckanext/multilingual/solr/french_stop.txt | 183 + .../ckanext/multilingual/solr/german_stop.txt | 292 + .../multilingual/solr/greek_stopwords.txt | 76 + .../multilingual/solr/italian_stop.txt | 301 + .../ckanext/multilingual/solr/polish_stop.txt | 186 + .../multilingual/solr/portuguese_stop.txt | 251 + .../multilingual/solr/romanian_stop.txt | 233 + .../ckanext/multilingual/solr/schema.xml | 486 + .../multilingual/solr/spanish_stop.txt | 354 + .../tests/test_multilingual_plugin.py | 239 + .../ckanext/reclineview/__init__.py | 0 .../reclineview/config_declaration.yaml | 14 + .../ckanext/reclineview/plugin.py | 306 + .../ckanext/reclineview/tests/__init__.py | 0 .../ckanext/reclineview/tests/test_view.py | 152 + .../reclineview/theme/public/css/recline.css | 380 + .../theme/public/css/recline.min.css | 1 + .../theme/public/img/ajaxload-circle.gif | Bin 0 -> 4176 bytes .../reclineview/theme/public/recline_view.js | 247 + .../theme/public/recline_view.min.js | 15 + .../reclineview/theme/public/resource.config | 15 + .../public/vendor/backbone/1.0.0/backbone.js | 1571 + .../bootstrap/3.2.0/css/bootstrap-theme.css | 442 + .../vendor/bootstrap/3.2.0/css/bootstrap.css | 6203 ++++ .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20335 bytes .../fonts/glyphicons-halflings-regular.svg | 229 + .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 41280 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23320 bytes .../vendor/bootstrap/3.2.0/js/bootstrap.js | 2114 ++ .../theme/public/vendor/ckan.js/ckan.js | 262 + .../theme/public/vendor/flot/excanvas.js | 1428 + .../theme/public/vendor/flot/excanvas.min.js | 1 + .../theme/public/vendor/flot/jquery.flot.js | 3061 ++ .../public/vendor/flot/jquery.flot.time.js | 431 + .../theme/public/vendor/flotr2/flotr2.js | 6128 ++++ .../theme/public/vendor/flotr2/flotr2.min.js | 489 + .../public/vendor/jquery/1.7.1/jquery.js | 9266 ++++++ .../public/vendor/jquery/1.7.1/jquery.min.js | 4 + .../theme/public/vendor/json/json2.js | 486 + .../theme/public/vendor/json/json2.min.js | 25 + .../MarkerCluster.Default.css | 60 + .../leaflet.markercluster/MarkerCluster.css | 6 + .../leaflet.markercluster-src.js | 2163 ++ .../leaflet.markercluster.js | 6 + .../vendor/leaflet/0.7.7/images/layers-2x.png | Bin 0 -> 1585 bytes .../vendor/leaflet/0.7.7/images/layers.png | Bin 0 -> 913 bytes .../leaflet/0.7.7/images/marker-icon-2x.png | Bin 0 -> 4032 bytes .../leaflet/0.7.7/images/marker-icon.png | Bin 0 -> 1747 bytes .../leaflet/0.7.7/images/marker-shadow.png | Bin 0 -> 681 bytes .../vendor/leaflet/0.7.7/leaflet-src.js | 9168 ++++++ .../public/vendor/leaflet/0.7.7/leaflet.css | 479 + .../public/vendor/leaflet/0.7.7/leaflet.js | 9 + .../public/vendor/moment/2.0.0/moment.js | 1400 + .../vendor/mustache/0.5.0-dev/mustache.js | 536 + .../vendor/mustache/0.5.0-dev/mustache.min.js | 36 + .../theme/public/vendor/recline/flot.css | 26 + .../theme/public/vendor/recline/map.css | 28 + .../theme/public/vendor/recline/recline.css | 644 + .../public/vendor/recline/recline.dataset.js | 896 + .../theme/public/vendor/recline/recline.js | 4463 +++ .../theme/public/vendor/recline/slickgrid.css | 188 + .../vendor/showdown/20120615/showdown.js | 1341 + .../vendor/showdown/20120615/showdown.min.js | 48 + .../vendor/slickgrid/2.2/MIT-LICENSE.txt | 20 + .../public/vendor/slickgrid/2.2/README.md | 25 + .../2.2/controls/slick.columnpicker.css | 31 + .../2.2/controls/slick.columnpicker.js | 152 + .../slickgrid/2.2/controls/slick.pager.css | 41 + .../slickgrid/2.2/controls/slick.pager.js | 154 + .../images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 86 bytes .../images/ui-bg_flat_75_ffffff_40x100.png | Bin 0 -> 74 bytes .../images/ui-bg_glass_55_fbf9ee_1x400.png | Bin 0 -> 111 bytes .../images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 90 bytes .../images/ui-bg_glass_75_dadada_1x400.png | Bin 0 -> 102 bytes .../images/ui-bg_glass_75_e6e6e6_1x400.png | Bin 0 -> 102 bytes .../images/ui-bg_glass_95_fef1ec_1x400.png | Bin 0 -> 115 bytes .../ui-bg_highlight-soft_75_cccccc_1x100.png | Bin 0 -> 86 bytes .../images/ui-icons_222222_256x240.png | Bin 0 -> 3687 bytes .../images/ui-icons_2e83ff_256x240.png | Bin 0 -> 3687 bytes .../images/ui-icons_454545_256x240.png | Bin 0 -> 3687 bytes .../images/ui-icons_888888_256x240.png | Bin 0 -> 3687 bytes .../images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 3687 bytes .../smoothness/jquery-ui-1.8.16.custom.css | 409 + .../vendor/slickgrid/2.2/images/actions.gif | Bin 0 -> 170 bytes .../2.2/images/ajax-loader-small.gif | Bin 0 -> 1849 bytes .../slickgrid/2.2/images/arrow_redo.png | Bin 0 -> 550 bytes .../2.2/images/arrow_right_peppermint.png | Bin 0 -> 126 bytes .../2.2/images/arrow_right_spearmint.png | Bin 0 -> 126 bytes .../slickgrid/2.2/images/arrow_undo.png | Bin 0 -> 555 bytes .../slickgrid/2.2/images/bullet_blue.png | Bin 0 -> 231 bytes .../slickgrid/2.2/images/bullet_star.png | Bin 0 -> 273 bytes .../2.2/images/bullet_toggle_minus.png | Bin 0 -> 154 bytes .../2.2/images/bullet_toggle_plus.png | Bin 0 -> 156 bytes .../vendor/slickgrid/2.2/images/calendar.gif | Bin 0 -> 1035 bytes .../vendor/slickgrid/2.2/images/collapse.gif | Bin 0 -> 846 bytes .../slickgrid/2.2/images/comment_yellow.gif | Bin 0 -> 257 bytes .../vendor/slickgrid/2.2/images/down.gif | Bin 0 -> 59 bytes .../slickgrid/2.2/images/drag-handle.png | Bin 0 -> 98 bytes .../slickgrid/2.2/images/editor-helper-bg.gif | Bin 0 -> 1164 bytes .../vendor/slickgrid/2.2/images/expand.gif | Bin 0 -> 851 bytes .../vendor/slickgrid/2.2/images/header-bg.gif | Bin 0 -> 872 bytes .../2.2/images/header-columns-bg.gif | Bin 0 -> 836 bytes .../2.2/images/header-columns-over-bg.gif | Bin 0 -> 823 bytes .../vendor/slickgrid/2.2/images/help.png | Bin 0 -> 328 bytes .../vendor/slickgrid/2.2/images/info.gif | Bin 0 -> 80 bytes .../vendor/slickgrid/2.2/images/listview.gif | Bin 0 -> 2380 bytes .../vendor/slickgrid/2.2/images/pencil.gif | Bin 0 -> 914 bytes .../slickgrid/2.2/images/row-over-bg.gif | Bin 0 -> 823 bytes .../vendor/slickgrid/2.2/images/sort-asc.gif | Bin 0 -> 830 bytes .../vendor/slickgrid/2.2/images/sort-asc.png | Bin 0 -> 104 bytes .../vendor/slickgrid/2.2/images/sort-desc.gif | Bin 0 -> 833 bytes .../vendor/slickgrid/2.2/images/sort-desc.png | Bin 0 -> 106 bytes .../vendor/slickgrid/2.2/images/stripes.png | Bin 0 -> 94 bytes .../vendor/slickgrid/2.2/images/tag_red.png | Bin 0 -> 529 bytes .../vendor/slickgrid/2.2/images/tick.png | Bin 0 -> 465 bytes .../slickgrid/2.2/images/user_identity.gif | Bin 0 -> 905 bytes .../2.2/images/user_identity_plus.gif | Bin 0 -> 546 bytes .../vendor/slickgrid/2.2/jquery-1.7.min.js | 4 + .../slickgrid/2.2/jquery-ui-1.8.16.custom.js | 611 + .../slickgrid/2.2/jquery.event.drag-2.2.js | 402 + .../slickgrid/2.2/jquery.event.drop-2.2.js | 302 + .../2.2/plugins/slick.autotooltips.js | 83 + .../2.2/plugins/slick.cellcopymanager.js | 86 + .../2.2/plugins/slick.cellrangedecorator.js | 66 + .../2.2/plugins/slick.cellrangeselector.js | 113 + .../2.2/plugins/slick.cellselectionmodel.js | 154 + .../2.2/plugins/slick.checkboxselectcolumn.js | 153 + .../2.2/plugins/slick.headerbuttons.css | 39 + .../2.2/plugins/slick.headerbuttons.js | 177 + .../2.2/plugins/slick.headermenu.css | 59 + .../slickgrid/2.2/plugins/slick.headermenu.js | 275 + .../2.2/plugins/slick.rowmovemanager.js | 138 + .../2.2/plugins/slick.rowselectionmodel.js | 187 + .../slickgrid/2.2/slick-default-theme.css | 118 + .../public/vendor/slickgrid/2.2/slick.core.js | 467 + .../vendor/slickgrid/2.2/slick.dataview.js | 1126 + .../vendor/slickgrid/2.2/slick.editors.js | 512 + .../vendor/slickgrid/2.2/slick.formatters.js | 59 + .../vendor/slickgrid/2.2/slick.grid.css | 157 + .../public/vendor/slickgrid/2.2/slick.grid.js | 3422 +++ .../2.2/slick.groupitemmetadataprovider.js | 158 + .../vendor/slickgrid/2.2/slick.remotemodel.js | 173 + .../theme/public/vendor/timeline/LICENSE | 365 + .../theme/public/vendor/timeline/README | 1 + .../public/vendor/timeline/css/loading.gif | Bin 0 -> 6909 bytes .../public/vendor/timeline/css/timeline.css | 284 + .../public/vendor/timeline/css/timeline.png | Bin 0 -> 14048 bytes .../vendor/timeline/css/timeline@2x.png | Bin 0 -> 36430 bytes .../public/vendor/timeline/js/timeline.js | 10015 ++++++ .../0.4.0/underscore.deferred.js | 445 + .../0.4.0/underscore.deferred.min.js | 17 + .../vendor/underscore/1.4.4/underscore.js | 1227 + .../reclineview/theme/public/webassets.yml | 47 + .../theme/public/widget.recordcount.js | 34 + .../theme/public/widget.recordcount.min.js | 3 + .../theme/templates/recline_graph_form.html | 8 + .../theme/templates/recline_map_form.html | 11 + .../theme/templates/recline_view.html | 24 + .../ckanext/resourceproxy/__init__.py | 0 .../ckanext/resourceproxy/blueprint.py | 115 + .../resourceproxy/config_declaration.yaml | 22 + .../ckanext/resourceproxy/plugin.py | 76 + .../ckanext/resourceproxy/tests/__init__.py | 0 .../resourceproxy/tests/static/huge.json | 3 + .../resourceproxy/tests/static/test.json | 5 + .../ckanext/resourceproxy/tests/test_proxy.py | 178 + .../ckanext/stats/__init__.py | 3 + .../ckanext/stats/blueprint.py | 55 + .../ckanext/stats/plugin.py | 24 + .../ckanext/stats/public/.gitignore | 2 + .../ckanext/stats/public/__init__.py | 9 + .../ckanext/stats/public/ckanext/__init__.py | 9 + .../stats/public/ckanext/stats/__init__.py | 9 + .../stats/public/ckanext/stats/css/stats.css | 16 + .../ckanext/stats/javascript/modules/plot.js | 209 + .../stats/javascript/modules/stats-nav.js | 39 + .../public/ckanext/stats/resource.config | 12 + .../ckanext/stats/test/fixtures/table.html | 30 + .../public/ckanext/stats/test/index.html | 59 + .../stats/test/spec/modules/plot.spec.js | 136 + .../stats/test/spec/modules/stats-nav.spec.js | 44 + .../public/ckanext/stats/vendor/excanvas.js | 1427 + .../ckanext/stats/vendor/jquery.flot.js | 2599 ++ .../stats/public/ckanext/stats/webassets.yml | 16 + .../ckanext/stats/stats.py | 405 + .../stats/templates/ckanext/stats/index.html | 178 + .../ckanext/stats/tests/__init__.py | 0 .../ckanext/stats/tests/test_stats_lib.py | 207 + .../ckanext/stats/tests/test_stats_plugin.py | 11 + .../ckanext/videoview/__init__.py | 0 .../ckanext/videoview/plugin.py | 52 + .../ckanext/videoview/tests/__init__.py | 0 .../ckanext/videoview/tests/test_view.py | 29 + .../videoview/theme/templates/video_form.html | 4 + .../videoview/theme/templates/video_view.html | 12 + .../ckanext/webpageview/__init__.py | 0 .../ckanext/webpageview/plugin.py | 45 + .../ckanext/webpageview/tests/__init__.py | 0 .../ckanext/webpageview/tests/test_view.py | 34 + .../theme/templates/webpage_form.html | 3 + .../theme/templates/webpage_view.html | 3 + .../ckanext_d4science_theme.egg-info/PKG-INFO | 148 + .../SOURCES.txt | 429 + .../dependency_links.txt | 1 + .../entry_points.txt | 6 + .../top_level.txt | 1 + .../ckanext-d4science_theme/requirements.txt | 3 + .../src/ckanext-d4science_theme/setup.cfg | 44 + .../2.10/src/ckanext-d4science_theme/setup.py | 102 + .../2.10/src/ckanext-d4science_theme/test.ini | 49 + images/ckan/2.10/setup/app/start_ckan.sh | 2 +- 941 files changed, 166127 insertions(+), 8 deletions(-) rename compose/2.10/{.ckan-env => .ckan.env} (89%) create mode 100644 compose/2.10/Dockerfile create mode 100644 compose/2.10/docker-entrypoint.d/01_setup_spatial.sh create mode 100644 compose/2.10/docker-entrypoint.d/02_setup_extensions.sh create mode 100644 compose/2.10/requirements.txt create mode 100644 compose/2.10/src/ckanext-d4science/.coveragerc create mode 100644 compose/2.10/src/ckanext-d4science/.github/workflows/test.yml create mode 100644 compose/2.10/src/ckanext-d4science/.gitignore create mode 100644 compose/2.10/src/ckanext-d4science/LICENSE create mode 100644 compose/2.10/src/ckanext-d4science/MANIFEST.in create mode 100644 compose/2.10/src/ckanext-d4science/README.md create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/.gitignore create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/script.js create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/style.css create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/webassets.yml create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/cli.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/helpers.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/i18n/.gitignore create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/action.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/auth.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/schema.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/validators.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/public/.gitignore create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/templates/.gitignore create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/logic/test_action.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/logic/test_auth.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/logic/test_validators.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/test_helpers.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/test_plugin.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/test_views.py create mode 100644 compose/2.10/src/ckanext-d4science/ckanext/d4science/views.py create mode 100644 compose/2.10/src/ckanext-d4science/dev-requirements.txt create mode 100644 compose/2.10/src/ckanext-d4science/requirements.txt create mode 100644 compose/2.10/src/ckanext-d4science/setup.cfg create mode 100644 compose/2.10/src/ckanext-d4science/setup.py create mode 100644 compose/2.10/src/ckanext-d4science/test.ini create mode 100644 compose/2.10/src/ckanext-d4science_theme/.project create mode 100644 compose/2.10/src/ckanext-d4science_theme/.settings/org.eclipse.core.resources.prefs create mode 100644 compose/2.10/src/ckanext-d4science_theme/LICENSE create mode 100644 compose/2.10/src/ckanext-d4science_theme/MANIFEST.in create mode 100644 compose/2.10/src/ckanext-d4science_theme/README.rst create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity-stream.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity.css.map create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity.scss create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/dashboard.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/dashboard.spec.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/webassets.yml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/changes.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/email_notifications.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/helpers.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/action.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/auth.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/schema.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/validators.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/model/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/model/activity.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/public/dotted.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/subscriptions.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/activity_streams/activity_stream_email_notifications.text create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/ajax_snippets/dashboard.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/activity_stream.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/changes.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/read_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/snippets/item_group.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/header.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/activity_stream.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/changes.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/read_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/snippets/item_organization.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/activity_stream.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/changes.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/history.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/read_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/resource_history.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/change_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/resource_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/resources.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/resources_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/page.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/added_tag.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_group.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_organization.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_package.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_resource.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_user.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_group.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_organization.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_package.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_resource.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/fallback.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/follow_dataset.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/follow_group.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/follow_user.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_group.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_organization.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_package.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_resource.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_user.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/removed_tag.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activity_type_selector.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/author.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/author_email.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/delete_resource.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/extension_fields.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/extra_fields.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/license.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/maintainer.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/maintainer_email.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/name.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/new_file.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/new_resource.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/no_change.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/notes.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/org.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/private.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_desc.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_extras.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_format.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_name.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/tags.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/title.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/url.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/version.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/description.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/image_url.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/no_change.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/title.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/description.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/image_url.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/no_change.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/title.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/pagination.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/stream.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/activity_stream.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/dashboard.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/edit_user_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/read_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/snippets/followee_dropdown.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/conftest.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_action.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_auth.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_functional.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_pagination.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/model/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/model/test_activity.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/test_changes.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/test_email_notifications.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/test_helpers.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/test_views.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/activity/views.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/audioview/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/audioview/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/audioview/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/audioview/tests/test_view.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/audioview/theme/templates/audio_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/audioview/theme/templates/audio_view.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/tests/test_plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/assets/css/d4science_theme.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/assets/js/d4science_scripts.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/assets/webassets.yml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/home.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/organization.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/systemtype.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_cache_controller.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_extras.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_namespaces.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_namespaces_controller.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_namespaces_extras_util.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/icproxycontroller.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/.gitignore create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/d4science_scripts.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/d4science_theme.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/helpers.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/.gitignore create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/D4ScienceDataCataloguelogo.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/D4ScienceDataCataloguelogo2.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/bg-noise.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/bg-pattern.svg create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/ckan-logo-footer.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/d4ScienceDataCatalogue.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/d4s_tagcloud.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/d4science.ico create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/d4science_logo.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/favicon.ico create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/gCube_70.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/gcubedatacataloguelogo.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/grsf/GRSF_for_admins_logo.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/groups/icon/placeholder-group.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/d4sciencelabs.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/emodnet.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/fao.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/grsf.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/d4sciencelabs.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/emodnet.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/fao.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/grsf.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/imarine.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/placeholder-organization.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/rprototypinglab.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/imarine.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/rprototypinglab.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/codelist.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/dataset.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/dsd.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/metadata.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/placeholder-type.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/researchobject.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/series.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/service.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/trainingmaterial.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/virtualresearchenvironment.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/placeholder-type.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/jquery.tagcloud.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/location_to_bboxes.ini create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/oai2_style.xsl create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/pageloading.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/parthenos/logo-parthenos.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/placeholder_types.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/qrcodelink/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/qrcodelink/generate_qrcode.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/activity_streams/activity_stream_email_notifications.text create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/config.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/confirm_reset.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/index.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/snippets/confirm_delete.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/snippets/data_type.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/trash.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/ajax_snippets/custom_fields.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/ajax_snippets/follow_button.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/dataviewer/base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/primer.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/actions.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/breadcrumb.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/context.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/facet.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/form_stages.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/media_grid.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/module.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/nav.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/page_header.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/pagination.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/simple-input.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/invite_user.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/invite_user_subject.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/reset_password.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/reset_password_subject.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/error_document_template.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/footer.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/about.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/admins.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/base_form_page.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/confirm_delete.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/confirm_delete_member.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/edit.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/edit_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/followers.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/index.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/member_new.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/members.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/new.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/new_group_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/read.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/read_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/feeds.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/group_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/group_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/group_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/helper.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/info.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/header.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/about.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/index.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/layout1.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/layout2.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/layout3.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/robots.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/about_text.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/featured_group.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/featured_organization.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/promoted.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/search.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/stats.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/autoform.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/attributes.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/checkbox.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/custom.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/errors.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/hidden.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/hidden_from_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/image_upload.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/info.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/input.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/input_block.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/markdown.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/prepend.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/required_message.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/select.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/textarea.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/about.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/admins.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/base_form_page.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/bulk_process.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/confirm_delete.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/confirm_delete_member.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/edit.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/edit_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/index.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/member_new.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/members.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/new.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/new_organization_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/read.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/read_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/snippets/feeds.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/snippets/helper.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/snippets/organization_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/snippets/organization_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization/snippets/organization_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization_vre/index.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/organization_vre/read.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/base_form_page.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/collaborators/collaborator_new.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/collaborators/collaborators.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/collaborators/confirm_delete.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/confirm_delete.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/confirm_delete_resource.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/edit.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/edit_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/edit_view.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/followers.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/group_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/new.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/new_package_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/new_resource.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/new_resource_not_draft.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/new_view.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/read.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/read_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/resource_edit.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/resource_edit_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/resource_read.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/resource_views.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/resources.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/search.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/additional_info.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/cannot_create_package.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/info.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/new_package_breadcrumb.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/package_basic_fields.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/package_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/package_metadata_fields.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_edit_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_help.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_info.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_upload_field.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_view.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_view_embed.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_view_filters.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_views_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resource_views_list_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resources.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/resources_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/stages.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/tags.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/view_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/view_form_filters.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/snippets/view_help.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/package/view_edit_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/page.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/revision/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/add_dataset.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/additional_info.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/context.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/context/dataset.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/context/group.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/context/user.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/csrf_input.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/custom_form_fields.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/datapusher_status.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/disqus_trackback.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/facet_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/follow_button.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/group.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/group_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/home_breadcrumb_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/language_selector.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/license.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/local_friendly_datetime.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/organization.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/organization_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/package_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/package_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/popular.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/private.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/search_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/search_result_text.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/simple_search.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/social.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/snippets/tag_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/tests/broken_helper_as_attribute.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/tests/broken_helper_as_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/tests/flash_messages.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/tests/helper_as_attribute.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/tests/helper_as_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/tests/mock_json_resource_preview_template.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/tests/mock_resource_preview_template.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/api_tokens.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/confirm_delete.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/dashboard.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/dashboard_datasets.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/dashboard_groups.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/dashboard_organizations.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/edit.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/edit_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/edit_user_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/followers.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/login.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/logout.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/logout_first.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/new.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/new_user_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/perform_reset.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/read.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/read_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/request_reset.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/snippets/api_token_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/snippets/followers.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/snippets/login_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/snippets/placeholder.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/snippets/recaptcha.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/user/snippets/user_search.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/.gitignore create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/footer.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/group/read_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/group/snippets/group_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/group/snippets/group_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/group/snippets/group_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/group/snippets/group_list_simple.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/group/snippets/group_tree.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/header.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/index.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/layout2.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/snippets/featured_organization.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/snippets/popular_formats.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/snippets/popular_groups.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/snippets/popular_metadatatypes.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/snippets/popular_tags.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/snippets/promoted.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/snippets/search.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/snippets/search_for_groups.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/snippets/search_for_location.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/snippets/search_for_organisations.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/snippets/search_for_types.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/home/snippets/stats.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/organization/read.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/organization/read_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/organization/snippets/organization_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/organization_vre/index.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/organization_vre/read.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/group_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/read.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/read_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/resource_edit_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/resource_read.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/search.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/snippets/additional_info.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/snippets/extras_table.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/snippets/info.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/snippets/qrcode_show.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/snippets/resource_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/snippets/resource_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/snippets/resources.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/package/snippets/resources_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/page.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/snippets/custom_form_fields.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/snippets/facet_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/snippets/package_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/snippets/tag_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/type/index.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/type/snippets/helper.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/type/snippets/type_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/type/snippets/type_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/type/snippets/type_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/user/dashboard.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/user/dashboard_datasets.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/user/dashboard_groups.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates2.6/user/dashboard_organizations.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/tests/test_plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/assets/datapusher.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/assets/datapusher.css.map create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/assets/datapusher.scss create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/assets/datapusher_popover.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/assets/webassets.yml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/cli.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/config_declaration.yaml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/helpers.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/interfaces.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/logic/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/logic/action.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/logic/auth.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/logic/schema.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/public/dotted.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/templates-bs3/datapusher/resource_data.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/templates-bs3/package/resource_edit_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/templates/datapusher/resource_data.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/templates/package/resource_edit_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/tests/test.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/tests/test_action.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/tests/test_default_views.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/tests/test_interfaces.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/tests/test_views.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datapusher/views.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/allowed_functions.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/backend/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/backend/postgres.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/blueprint.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/cli.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/config_declaration.yaml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/helpers.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/interfaces.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/logic/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/logic/action.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/logic/auth.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/logic/schema.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/set_permissions.sql create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/ajax_snippets/api_info.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/datastore/api_examples/curl.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/datastore/api_examples/javascript.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/datastore/api_examples/powershell.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/datastore/api_examples/python.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/datastore/api_examples/r.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/datastore/dictionary.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/datastore/snippets/dictionary_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/package/resource_edit_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/package/resource_read.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/package/snippets/data_api_button.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/package/snippets/dictionary_table.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/package/snippets/resource_item.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates-bs3/package/snippets/resources.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates/ajax_snippets/api_info.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates/datastore/dictionary.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates/datastore/snippets/dictionary_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates/package/resource_edit_base.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates/package/resource_read.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates/package/snippets/data_api_button.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/templates/package/snippets/dictionary_table.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/allowed_functions.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/conftest.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/helpers.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/sample_datastore_plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_auth.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_chained_action.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_chained_auth_functions.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_create.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_db.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_delete.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_dictionary.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_disable.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_dump.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_helpers.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_info.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_interface.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_search.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_unit.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/tests/test_upsert.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datastore/writer.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/blueprint.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/config_declaration.yaml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/datatables_view.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/datatablesview.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/resource.config create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/dataTables.scrollResize.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/datatables.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/datatables.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/datatables.mark.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/datatables.mark.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/datetime.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/af.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/am.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ar.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/az.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/az_az.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/be.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/bg.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/bn.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/bs.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ca.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/cs.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/cs_CZ.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/cy.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/da.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/de.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/el.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/en-gb.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/en_GB.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/eo.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/es.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/et.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/eu.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/fa.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/fi.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/fil.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/fr.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ga.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/gl.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/gu.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/he.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/hi.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/hr.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/hu.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/hy.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/id.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/id_alt.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/is.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/it.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ja.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ka.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/kk.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/km.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/kn.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ko.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ko_KR.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ku.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ky.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/lo.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/lt.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/lv.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/mk.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/mn.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ms.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ne.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/nl.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/no.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/no_nb.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/pa.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/pl.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ps.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/pt.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/pt_BR.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/pt_pt.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/rm.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ro.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ru.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/si.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/sk.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/sl.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/snd.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/sq.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/sr.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/sr@latin.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/sr_sp.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/sv.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/sw.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ta.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/te.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/tg.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/th.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/tl.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/tr.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/uk.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/ur.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/uz.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/vi.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/zh.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/zh_CN.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/i18n/zh_Hant.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/DataTables/jquery.mark.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/vendor/FontAwesome/images/times-circle-solid.svg create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/public/webassets.yml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/templates/datatables/datatables_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/datatablesview/templates/datatables/datatables_view.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/expire_api_token/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/expire_api_token/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/expire_api_token/templates/user/api_tokens.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/expire_api_token/templates/user/snippets/api_token_list.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/expire_api_token/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/expire_api_token/tests/test_plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/imageview/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/imageview/config_declaration.yaml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/imageview/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/imageview/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/imageview/tests/test_view.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/imageview/theme/templates/image_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/imageview/theme/templates/image_view.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/solr/dutch_stop.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/solr/english_stop.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/solr/fr_elision.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/solr/french_stop.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/solr/german_stop.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/solr/greek_stopwords.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/solr/italian_stop.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/solr/polish_stop.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/solr/portuguese_stop.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/solr/romanian_stop.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/solr/schema.xml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/solr/spanish_stop.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/multilingual/tests/test_multilingual_plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/config_declaration.yaml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/tests/test_view.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/css/recline.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/css/recline.min.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/img/ajaxload-circle.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/recline_view.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/recline_view.min.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/resource.config create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/backbone/1.0.0/backbone.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/bootstrap/3.2.0/css/bootstrap-theme.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/bootstrap/3.2.0/css/bootstrap.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/bootstrap/3.2.0/fonts/glyphicons-halflings-regular.eot create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/bootstrap/3.2.0/fonts/glyphicons-halflings-regular.svg create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/bootstrap/3.2.0/fonts/glyphicons-halflings-regular.ttf create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/bootstrap/3.2.0/fonts/glyphicons-halflings-regular.woff create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/bootstrap/3.2.0/js/bootstrap.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/ckan.js/ckan.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/flot/excanvas.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/flot/excanvas.min.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/flot/jquery.flot.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/flot/jquery.flot.time.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/flotr2/flotr2.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/flotr2/flotr2.min.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/jquery/1.7.1/jquery.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/jquery/1.7.1/jquery.min.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/json/json2.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/json/json2.min.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/leaflet.markercluster/MarkerCluster.Default.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/leaflet.markercluster/MarkerCluster.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/leaflet.markercluster/leaflet.markercluster-src.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/leaflet.markercluster/leaflet.markercluster.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/leaflet/0.7.7/images/layers-2x.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/leaflet/0.7.7/images/layers.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/leaflet/0.7.7/images/marker-icon-2x.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/leaflet/0.7.7/images/marker-icon.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/leaflet/0.7.7/images/marker-shadow.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/leaflet/0.7.7/leaflet-src.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/leaflet/0.7.7/leaflet.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/leaflet/0.7.7/leaflet.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/moment/2.0.0/moment.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/mustache/0.5.0-dev/mustache.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/mustache/0.5.0-dev/mustache.min.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/recline/flot.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/recline/map.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/recline/recline.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/recline/recline.dataset.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/recline/recline.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/recline/slickgrid.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/showdown/20120615/showdown.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/showdown/20120615/showdown.min.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/MIT-LICENSE.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/README.md create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/controls/slick.columnpicker.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/controls/slick.columnpicker.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/controls/slick.pager.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/controls/slick.pager.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-icons_222222_256x240.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-icons_2e83ff_256x240.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-icons_454545_256x240.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-icons_888888_256x240.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/images/ui-icons_cd0a0a_256x240.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/css/smoothness/jquery-ui-1.8.16.custom.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/actions.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/ajax-loader-small.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/arrow_redo.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/arrow_right_peppermint.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/arrow_right_spearmint.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/arrow_undo.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/bullet_blue.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/bullet_star.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/bullet_toggle_minus.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/bullet_toggle_plus.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/calendar.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/collapse.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/comment_yellow.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/down.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/drag-handle.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/editor-helper-bg.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/expand.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/header-bg.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/header-columns-bg.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/header-columns-over-bg.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/help.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/info.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/listview.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/pencil.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/row-over-bg.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/sort-asc.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/sort-asc.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/sort-desc.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/sort-desc.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/stripes.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/tag_red.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/tick.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/user_identity.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/images/user_identity_plus.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/jquery-1.7.min.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/jquery-ui-1.8.16.custom.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/jquery.event.drag-2.2.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/jquery.event.drop-2.2.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/plugins/slick.autotooltips.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/plugins/slick.cellcopymanager.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/plugins/slick.cellrangedecorator.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/plugins/slick.cellrangeselector.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/plugins/slick.cellselectionmodel.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/plugins/slick.checkboxselectcolumn.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/plugins/slick.headerbuttons.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/plugins/slick.headerbuttons.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/plugins/slick.headermenu.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/plugins/slick.headermenu.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/plugins/slick.rowmovemanager.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/plugins/slick.rowselectionmodel.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick-default-theme.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick.core.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick.dataview.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick.editors.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick.formatters.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick.grid.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick.grid.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick.groupitemmetadataprovider.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/slickgrid/2.2/slick.remotemodel.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/timeline/LICENSE create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/timeline/README create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/timeline/css/loading.gif create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/timeline/css/timeline.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/timeline/css/timeline.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/timeline/css/timeline@2x.png create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/timeline/js/timeline.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/underscore.deferred/0.4.0/underscore.deferred.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/underscore.deferred/0.4.0/underscore.deferred.min.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/vendor/underscore/1.4.4/underscore.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/webassets.yml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/widget.recordcount.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/public/widget.recordcount.min.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/templates/recline_graph_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/templates/recline_map_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/reclineview/theme/templates/recline_view.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/resourceproxy/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/resourceproxy/blueprint.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/resourceproxy/config_declaration.yaml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/resourceproxy/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/resourceproxy/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/resourceproxy/tests/static/huge.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/resourceproxy/tests/static/test.json create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/resourceproxy/tests/test_proxy.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/blueprint.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/.gitignore create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/stats/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/stats/css/stats.css create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/stats/javascript/modules/plot.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/stats/javascript/modules/stats-nav.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/stats/resource.config create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/stats/test/fixtures/table.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/stats/test/index.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/stats/test/spec/modules/plot.spec.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/stats/test/spec/modules/stats-nav.spec.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/stats/vendor/excanvas.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/stats/vendor/jquery.flot.js create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/public/ckanext/stats/webassets.yml create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/stats.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/templates/ckanext/stats/index.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/tests/test_stats_lib.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/stats/tests/test_stats_plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/videoview/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/videoview/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/videoview/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/videoview/tests/test_view.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/videoview/theme/templates/video_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/videoview/theme/templates/video_view.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/webpageview/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/webpageview/plugin.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/webpageview/tests/__init__.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/webpageview/tests/test_view.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/webpageview/theme/templates/webpage_form.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext/webpageview/theme/templates/webpage_view.html create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext_d4science_theme.egg-info/PKG-INFO create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext_d4science_theme.egg-info/SOURCES.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext_d4science_theme.egg-info/dependency_links.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext_d4science_theme.egg-info/entry_points.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/ckanext_d4science_theme.egg-info/top_level.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/requirements.txt create mode 100644 compose/2.10/src/ckanext-d4science_theme/setup.cfg create mode 100644 compose/2.10/src/ckanext-d4science_theme/setup.py create mode 100644 compose/2.10/src/ckanext-d4science_theme/test.ini diff --git a/compose/2.10/.ckan-env b/compose/2.10/.ckan.env similarity index 89% rename from compose/2.10/.ckan-env rename to compose/2.10/.ckan.env index b9f2598..4230d56 100644 --- a/compose/2.10/.ckan-env +++ b/compose/2.10/.ckan.env @@ -9,13 +9,13 @@ MAINTENANCE_MODE=false CKAN_SITE_ID=default CKAN_SITE_URL=http://localhost:5000 CKAN_PORT=5000 -CKAN_MAX_UPLOAD_SIZE_MB=20 +CKAN_MAX_UPLOAD_SIZE_MB=200 CKAN___BEAKER__SESSION__SECRET=CHANGE_ME # See https://docs.ckan.org/en/latest/maintaining/configuration.html#api-token-settings CKAN___API_TOKEN__JWT__ENCODE__SECRET=string:CHANGE_ME CKAN___API_TOKEN__JWT__DECODE__SECRET=string:CHANGE_ME # CKAN Plugins -CKAN__PLUGINS=envvars image_view text_view recline_view datastore datapusher +CKAN__PLUGINS="envvars image_view text_view recline_view datastore datapusher spatial_metadata spatial_query d4science d4science_theme" # CKAN requires storage path to be set in order for filestore to be enabled CKAN__STORAGE_PATH=/srv/app/data CKAN__WEBASSETS__PATH=/srv/app/data/webassets @@ -39,4 +39,4 @@ CKAN__DATAPUSHER__API_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ6M0l # Solr configuration CKAN_VERSION=2.10 -CKAN_CORE_NAME=ckan \ No newline at end of file +CKAN_CORE_NAME=ckan diff --git a/compose/2.10/Dockerfile b/compose/2.10/Dockerfile new file mode 100644 index 0000000..9a18e10 --- /dev/null +++ b/compose/2.10/Dockerfile @@ -0,0 +1,39 @@ +################### +### Extensions +################### +FROM maicol07/ckan:2.10.4-focal as extbuild + +# Switch to the root user +USER root + +#install pip +RUN apt-get update +RUN apt-get install python3-dev python3-pip libxml2-dev libxslt1-dev libgeos-c1v5 -y + +############ +### MAIN +############ +FROM maicol07/ckan:2.10.4-focal + +# Add the custom extensions to the plugins list +ENV CKAN__PLUGINS envvars image_view text_view recline_view datastore datapusher spatial_metadata spatial_query d4science d4science_theme + +# Switch to the root user +USER root + +#COPY --from=extbuild /wheels /srv/app/ext_wheels + +# Add requirements.txt to the image +COPY requirements.txt /srv/app/requirements.txt + +# Install and enable the custom extensions +RUN pip install -r /srv/app/requirements.txt + +RUN ckan config-tool ${APP_DIR}/production.ini "ckan.plugins = ${CKAN__PLUGINS}" && \ + chown -R ckan:ckan /srv/app + +# Remove wheels +RUN rm -rf /srv/app/ext_wheels + +# Switch to the ckan user +#USER ckan diff --git a/compose/2.10/docker-compose.yml b/compose/2.10/docker-compose.yml index 091a3e4..8288623 100644 --- a/compose/2.10/docker-compose.yml +++ b/compose/2.10/docker-compose.yml @@ -9,7 +9,8 @@ volumes: services: ckan: container_name: ckan - image: ghcr.io/keitaroinc/ckan:${CKAN_VERSION} + build: + context: . networks: - frontend - backend @@ -19,7 +20,7 @@ services: ports: - "0.0.0.0:${CKAN_PORT}:5000" env_file: - - ./.ckan-env + - ./.ckan.env environment: - CKAN_SQLALCHEMY_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/ckan - CKAN_DATASTORE_WRITE_URL=postgresql://ckan:${POSTGRES_PASSWORD}@db/datastore @@ -33,6 +34,8 @@ services: volumes: - ckan_data:/srv/app/data + - ./src:/srv/app/src_extensions + - ./docker-entrypoint.d:/srv/app/docker-entrypoint.d datapusher: container_name: datapusher @@ -73,11 +76,11 @@ services: solr: container_name: solr - image: solr:8.11.1 + image: ckan/ckan-solr:2.10-solr9-spatial networks: - backend env_file: - - ./.ckan-env + - ./.ckan.env environment: - CKAN_CORE_NAME=${CKAN_CORE_NAME} - CKAN_VERSION=${CKAN_VERSION} diff --git a/compose/2.10/docker-entrypoint.d/01_setup_spatial.sh b/compose/2.10/docker-entrypoint.d/01_setup_spatial.sh new file mode 100644 index 0000000..73af75a --- /dev/null +++ b/compose/2.10/docker-entrypoint.d/01_setup_spatial.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +if [[ $CKAN__PLUGINS == *"spatial_metadata"* ]]; then + echo "Configuring CKAN spatial ..." + + ckan config-tool ${APP_DIR}/production.ini "ckanext.spatial.common_map.type=Stadia.StamenTerrain" + ckan config-tool ${APP_DIR}/production.ini "ckanext.spatial.common_map.max_zoom=18" + # ckan config-tool $CKAN_INI "ckanext.spatial.search_backend = solr-spatial-field" + # ckan config-tool $CKAN_INI "ckan.search.solr_allowed_query_parsers = field" + # forse è da aggiungere un qualche tipo di key per production + +else + echo "Not configuring spatial, plugin not enabled." +fi diff --git a/compose/2.10/docker-entrypoint.d/02_setup_extensions.sh b/compose/2.10/docker-entrypoint.d/02_setup_extensions.sh new file mode 100644 index 0000000..7f6f9c8 --- /dev/null +++ b/compose/2.10/docker-entrypoint.d/02_setup_extensions.sh @@ -0,0 +1,42 @@ +SRC_EXTENSIONS_DIR=/srv/app/src_extensions + +# Install any local extensions in the src_extensions volume +echo "Looking for local extensions to install..." +echo "Extension dir ($SRC_EXTENSIONS_DIR) contents:" +ls -la $SRC_EXTENSIONS_DIR +for i in $SRC_EXTENSIONS_DIR/* +do + if [ -d $i ]; + then + + if [ -f $i/pip-requirements.txt ]; + then + pip install -r $i/pip-requirements.txt + echo "Found requirements file in $i" + fi + if [ -f $i/requirements.txt ]; + then + pip install -r $i/requirements.txt + echo "Found requirements file in $i" + fi + if [ -f $i/dev-requirements.txt ]; + then + pip install -r $i/dev-requirements.txt + echo "Found dev-requirements file in $i" + fi + if [ -f $i/setup.py ]; + then + cd $i + python3 $i/setup.py develop + echo "Found setup.py file in $i" + cd $APP_DIR + fi + + # Point `use` in test.ini to location of `test-core.ini` + if [ -f $i/test.ini ]; + then + echo "Updating \`test.ini\` reference to \`test-core.ini\` for plugin $i" + ckan config-tool $i/test.ini "use = config:../../src/ckan/test-core.ini" + fi + fi +done diff --git a/compose/2.10/requirements.txt b/compose/2.10/requirements.txt new file mode 100644 index 0000000..9db4c49 --- /dev/null +++ b/compose/2.10/requirements.txt @@ -0,0 +1,3 @@ +# Spatial +-e git+https://github.com/ckan/ckanext-spatial.git@v2.1.1#egg=ckanext-spatial +-r https://raw.githubusercontent.com/ckan/ckanext-spatial/v2.1.1/requirements.txt diff --git a/compose/2.10/src/ckanext-d4science/.coveragerc b/compose/2.10/src/ckanext-d4science/.coveragerc new file mode 100644 index 0000000..895e291 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/.coveragerc @@ -0,0 +1,5 @@ +[report] +omit = + */site-packages/* + */python?.?/* + ckan/* diff --git a/compose/2.10/src/ckanext-d4science/.github/workflows/test.yml b/compose/2.10/src/ckanext-d4science/.github/workflows/test.yml new file mode 100644 index 0000000..c920d38 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/.github/workflows/test.yml @@ -0,0 +1,48 @@ +name: Tests +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + container: + # The CKAN version tag of the Solr and Postgres containers should match + # the one of the container the tests run on. + # You can switch this base image with a custom image tailored to your project + image: ckan/ckan-dev:2.10 + services: + solr: + image: ckan/ckan-solr:2.10-solr9 + postgres: + image: ckan/ckan-postgres-dev:2.10 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + redis: + image: redis:3 + + env: + CKAN_SQLALCHEMY_URL: postgresql://ckan_default:pass@postgres/ckan_test + CKAN_DATASTORE_WRITE_URL: postgresql://datastore_write:pass@postgres/datastore_test + CKAN_DATASTORE_READ_URL: postgresql://datastore_read:pass@postgres/datastore_test + CKAN_SOLR_URL: http://solr:8983/solr/ckan + CKAN_REDIS_URL: redis://redis:6379/1 + + steps: + - uses: actions/checkout@v4 + - name: Install requirements + # Install any extra requirements your extension has here (dev requirements, other extensions etc) + run: | + pip install -r requirements.txt + pip install -r dev-requirements.txt + pip install -e . + - name: Setup extension + # Extra initialization steps + run: | + # Replace default path to CKAN core config file with the one on the container + sed -i -e 's/use = config:.*/use = config:\/srv\/app\/src\/ckan\/test-core.ini/' test.ini + + ckan -c test.ini db init + - name: Run tests + run: pytest --ckan-ini=test.ini --cov=ckanext.d4science --disable-warnings ckanext/d4science + diff --git a/compose/2.10/src/ckanext-d4science/.gitignore b/compose/2.10/src/ckanext-d4science/.gitignore new file mode 100644 index 0000000..8570dc5 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/.gitignore @@ -0,0 +1,42 @@ +.ropeproject +node_modules +bower_components + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +sdist/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Sphinx documentation +docs/_build/ diff --git a/compose/2.10/src/ckanext-d4science/LICENSE b/compose/2.10/src/ckanext-d4science/LICENSE new file mode 100644 index 0000000..58777e3 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/LICENSE @@ -0,0 +1,661 @@ +GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/compose/2.10/src/ckanext-d4science/MANIFEST.in b/compose/2.10/src/ckanext-d4science/MANIFEST.in new file mode 100644 index 0000000..3d68def --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/MANIFEST.in @@ -0,0 +1,5 @@ +include README.rst +include LICENSE +include requirements.txt +recursive-include ckanext/d4science *.html *.json *.js *.less *.css *.mo *.yml +recursive-include ckanext/d4science/migration *.ini *.py *.mako diff --git a/compose/2.10/src/ckanext-d4science/README.md b/compose/2.10/src/ckanext-d4science/README.md new file mode 100644 index 0000000..17f8649 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/README.md @@ -0,0 +1,123 @@ +[![Tests](https://github.com/d4science/ckanext-d4science/workflows/Tests/badge.svg?branch=main)](https://github.com/d4science/ckanext-d4science/actions) + +# ckanext-d4science + +**TODO:** Put a description of your extension here: What does it do? What features does it have? Consider including some screenshots or embedding a video! + + +## Requirements + +**TODO:** For example, you might want to mention here which versions of CKAN this +extension works with. + +If your extension works across different versions you can add the following table: + +Compatibility with core CKAN versions: + +| CKAN version | Compatible? | +| --------------- | ------------- | +| 2.6 and earlier | not tested | +| 2.7 | not tested | +| 2.8 | not tested | +| 2.9 | not tested | + +Suggested values: + +* "yes" +* "not tested" - I can't think of a reason why it wouldn't work +* "not yet" - there is an intention to get it working +* "no" + + +## Installation + +**TODO:** Add any additional install steps to the list below. + For example installing any non-Python dependencies or adding any required + config settings. + +To install ckanext-d4science: + +1. Activate your CKAN virtual environment, for example: + + . /usr/lib/ckan/default/bin/activate + +2. Clone the source and install it on the virtualenv + + git clone https://github.com/d4science/ckanext-d4science.git + cd ckanext-d4science + pip install -e . + pip install -r requirements.txt + +3. Add `d4science` to the `ckan.plugins` setting in your CKAN + config file (by default the config file is located at + `/etc/ckan/default/ckan.ini`). + +4. Restart CKAN. For example if you've deployed CKAN with Apache on Ubuntu: + + sudo service apache2 reload + + +## Config settings + +None at present + +**TODO:** Document any optional config settings here. For example: + + # The minimum number of hours to wait before re-checking a resource + # (optional, default: 24). + ckanext.d4science.some_setting = some_default_value + + +## Developer installation + +To install ckanext-d4science for development, activate your CKAN virtualenv and +do: + + git clone https://github.com/d4science/ckanext-d4science.git + cd ckanext-d4science + python setup.py develop + pip install -r dev-requirements.txt + + +## Tests + +To run the tests, do: + + pytest --ckan-ini=test.ini + + +## Releasing a new version of ckanext-d4science + +If ckanext-d4science should be available on PyPI you can follow these steps to publish a new version: + +1. Update the version number in the `setup.py` file. See [PEP 440](http://legacy.python.org/dev/peps/pep-0440/#public-version-identifiers) for how to choose version numbers. + +2. Make sure you have the latest version of necessary packages: + + pip install --upgrade setuptools wheel twine + +3. Create a source and binary distributions of the new version: + + python setup.py sdist bdist_wheel && twine check dist/* + + Fix any errors you get. + +4. Upload the source distribution to PyPI: + + twine upload dist/* + +5. Commit any outstanding changes: + + git commit -a + git push + +6. Tag the new release of the project on GitHub with the version number from + the `setup.py` file. For example if the version number in `setup.py` is + 0.0.1 then do: + + git tag 0.0.1 + git push --tags + +## License + +[AGPL](https://www.gnu.org/licenses/agpl-3.0.en.html) diff --git a/compose/2.10/src/ckanext-d4science/ckanext/__init__.py b/compose/2.10/src/ckanext-d4science/ckanext/__init__.py new file mode 100644 index 0000000..ed48ed0 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/__init__.py @@ -0,0 +1,9 @@ +# encoding: utf-8 + +# this is a namespace package +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + import pkgutil + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/__init__.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/.gitignore b/compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/script.js b/compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/script.js new file mode 100644 index 0000000..19cbb55 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/script.js @@ -0,0 +1,10 @@ +ckan.module("d4science-module", function ($, _) { + "use strict"; + return { + options: { + debug: false, + }, + + initialize: function () {}, + }; +}); diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/style.css b/compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/style.css new file mode 100644 index 0000000..9631595 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/style.css @@ -0,0 +1,5 @@ +/* +body { + border-radius: 0; +} +*/ diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/webassets.yml b/compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/webassets.yml new file mode 100644 index 0000000..6efc58d --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/assets/webassets.yml @@ -0,0 +1,14 @@ +# d4science-js: +# filter: rjsmin +# output: ckanext-d4science/%(version)s-d4science.js +# contents: +# - js/d4science.js +# extra: +# preload: +# - base/main + +# d4science-css: +# filter: cssrewrite +# output: ckanext-d4science/%(version)s-d4science.css +# contents: +# - css/d4science.css diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/cli.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/cli.py new file mode 100644 index 0000000..060483e --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/cli.py @@ -0,0 +1,20 @@ +import click + + +@click.group(short_help="d4science CLI.") +def d4science(): + """d4science CLI. + """ + pass + + +@d4science.command() +@click.argument("name", default="d4science") +def command(name): + """Docs. + """ + click.echo("Hello, {name}!".format(name=name)) + + +def get_commands(): + return [d4science] diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/helpers.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/helpers.py new file mode 100644 index 0000000..98b7f2d --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/helpers.py @@ -0,0 +1,9 @@ + +def d4science_hello(): + return "Hello, d4science!" + + +def get_helpers(): + return { + "d4science_hello": d4science_hello, + } diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/i18n/.gitignore b/compose/2.10/src/ckanext-d4science/ckanext/d4science/i18n/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/action.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/action.py new file mode 100644 index 0000000..abd4970 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/action.py @@ -0,0 +1,25 @@ +import ckan.plugins.toolkit as tk +import ckanext.d4science.logic.schema as schema + + +@tk.side_effect_free +def d4science_get_sum(context, data_dict): + tk.check_access( + "d4science_get_sum", context, data_dict) + data, errors = tk.navl_validate( + data_dict, schema.d4science_get_sum(), context) + + if errors: + raise tk.ValidationError(errors) + + return { + "left": data["left"], + "right": data["right"], + "sum": data["left"] + data["right"] + } + + +def get_actions(): + return { + 'd4science_get_sum': d4science_get_sum, + } diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/auth.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/auth.py new file mode 100644 index 0000000..346bef3 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/auth.py @@ -0,0 +1,12 @@ +import ckan.plugins.toolkit as tk + + +@tk.auth_allow_anonymous_access +def d4science_get_sum(context, data_dict): + return {"success": True} + + +def get_auth_functions(): + return { + "d4science_get_sum": d4science_get_sum, + } diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/schema.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/schema.py new file mode 100644 index 0000000..f5c4191 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/schema.py @@ -0,0 +1,11 @@ +import ckan.plugins.toolkit as tk + + +def d4science_get_sum(): + not_empty = tk.get_validator("not_empty") + convert_int = tk.get_validator("convert_int") + + return { + "left": [not_empty, convert_int], + "right": [not_empty, convert_int] + } diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/validators.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/validators.py new file mode 100644 index 0000000..8a7e597 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/logic/validators.py @@ -0,0 +1,13 @@ +import ckan.plugins.toolkit as tk + + +def d4science_required(value): + if not value or value is tk.missing: + raise tk.Invalid(tk._("Required")) + return value + + +def get_validators(): + return { + "d4science_required": d4science_required, + } diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/plugin.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/plugin.py new file mode 100644 index 0000000..06eb5a5 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/plugin.py @@ -0,0 +1,77 @@ +import ckan.plugins as plugins +import ckan.plugins.toolkit as toolkit +from ckan.views.resource import Blueprint +from ckanext.d4science import helpers +from ckanext.d4science.logic import action, auth +from ckanext.d4science.logic import validators + + +# import ckanext.d4science.cli as cli +# import ckanext.d4science.helpers as helpers +# import ckanext.d4science.views as views +# from ckanext.d4science.logic import ( +# action, auth, validators +# ) + + +class D4SciencePlugin(plugins.SingletonPlugin): + plugins.implements(plugins.IConfigurer) + + plugins.implements(plugins.IAuthFunctions) + plugins.implements(plugins.IActions) + # plugins.implements(plugins.IClick) + plugins.implements(plugins.ITemplateHelpers) + plugins.implements(plugins.IValidators) + + #ckan 2.10 + plugins.implements(plugins.IBlueprint) + + + # IConfigurer + + def update_config(self, config_): + toolkit.add_template_directory(config_, "templates") + toolkit.add_public_directory(config_, "public") + toolkit.add_resource("assets", "d4science") + + + # IAuthFunctions + + def get_auth_functions(self): + return auth.get_auth_functions() + + # IActions + + def get_actions(self): + return action.get_actions() + + # IBlueprint + + # def get_blueprint(self): + # return views.get_blueprints() + + def get_blueprint(self): + blueprint = Blueprint('foo', self.__module__) + # rules = [ + # ('/group', 'group_index', custom_group_index), + # ] + # for rule in rules: + # blueprint.add_url_rule(*rule) + + return blueprint + + # IClick + + # def get_commands(self): + # return cli.get_commands() + + # ITemplateHelpers + + def get_helpers(self): + return helpers.get_helpers() + + # IValidators + + def get_validators(self): + return validators.get_validators() + diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/public/.gitignore b/compose/2.10/src/ckanext-d4science/ckanext/d4science/public/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/templates/.gitignore b/compose/2.10/src/ckanext-d4science/ckanext/d4science/templates/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/__init__.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/logic/test_action.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/logic/test_action.py new file mode 100644 index 0000000..4983727 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/logic/test_action.py @@ -0,0 +1,13 @@ +"""Tests for action.py.""" + +import pytest + +import ckan.tests.helpers as test_helpers + + +@pytest.mark.ckan_config("ckan.plugins", "d4science") +@pytest.mark.usefixtures("with_plugins") +def test_d4science_get_sum(): + result = test_helpers.call_action( + "d4science_get_sum", left=10, right=30) + assert result["sum"] == 40 diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/logic/test_auth.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/logic/test_auth.py new file mode 100644 index 0000000..183d32b --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/logic/test_auth.py @@ -0,0 +1,19 @@ +"""Tests for auth.py.""" + +import pytest + +import ckan.tests.factories as factories +import ckan.tests.helpers as test_helpers +import ckan.model as model + + +@pytest.mark.ckan_config("ckan.plugins", "d4science") +@pytest.mark.usefixtures("with_request_context", "with_plugins", "clean_db") +def test_d4science_get_sum(): + user = factories.User() + context = { + "user": user["name"], + "model": model + } + assert test_helpers.call_auth( + "d4science_get_sum", context=context) diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/logic/test_validators.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/logic/test_validators.py new file mode 100644 index 0000000..6c7f28e --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/logic/test_validators.py @@ -0,0 +1,16 @@ +"""Tests for validators.py.""" + +import pytest + +import ckan.plugins.toolkit as tk + +from ckanext.d4science.logic import validators + + +def test_d4science_reauired_with_valid_value(): + assert validators.d4science_required("value") == "value" + + +def test_d4science_reauired_with_invalid_value(): + with pytest.raises(tk.Invalid): + validators.d4science_required(None) diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/test_helpers.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/test_helpers.py new file mode 100644 index 0000000..f90d94b --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/test_helpers.py @@ -0,0 +1,7 @@ +"""Tests for helpers.py.""" + +import ckanext.d4science.helpers as helpers + + +def test_d4science_hello(): + assert helpers.d4science_hello() == "Hello, d4science!" diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/test_plugin.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/test_plugin.py new file mode 100644 index 0000000..ee3f39f --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/test_plugin.py @@ -0,0 +1,56 @@ +""" +Tests for plugin.py. + +Tests are written using the pytest library (https://docs.pytest.org), and you +should read the testing guidelines in the CKAN docs: +https://docs.ckan.org/en/2.9/contributing/testing.html + +To write tests for your extension you should install the pytest-ckan package: + + pip install pytest-ckan + +This will allow you to use CKAN specific fixtures on your tests. + +For instance, if your test involves database access you can use `clean_db` to +reset the database: + + import pytest + + from ckan.tests import factories + + @pytest.mark.usefixtures("clean_db") + def test_some_action(): + + dataset = factories.Dataset() + + # ... + +For functional tests that involve requests to the application, you can use the +`app` fixture: + + from ckan.plugins import toolkit + + def test_some_endpoint(app): + + url = toolkit.url_for('myblueprint.some_endpoint') + + response = app.get(url) + + assert response.status_code == 200 + + +To temporary patch the CKAN configuration for the duration of a test you can use: + + import pytest + + @pytest.mark.ckan_config("ckanext.myext.some_key", "some_value") + def test_some_action(): + pass +""" +import ckanext.d4science.plugin as plugin + + +@pytest.mark.ckan_config("ckan.plugins", "d4science") +@pytest.mark.usefixtures("with_plugins") +def test_plugin(): + assert plugin_loaded("d4science") diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/test_views.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/test_views.py new file mode 100644 index 0000000..9f12530 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/tests/test_views.py @@ -0,0 +1,16 @@ +"""Tests for views.py.""" + +import pytest + +import ckanext.d4science.validators as validators + + +import ckan.plugins.toolkit as tk + + +@pytest.mark.ckan_config("ckan.plugins", "d4science") +@pytest.mark.usefixtures("with_plugins") +def test_d4science_blueprint(app, reset_db): + resp = app.get(tk.h.url_for("d4science.page")) + assert resp.status_code == 200 + assert resp.body == "Hello, d4science!" diff --git a/compose/2.10/src/ckanext-d4science/ckanext/d4science/views.py b/compose/2.10/src/ckanext-d4science/ckanext/d4science/views.py new file mode 100644 index 0000000..7192356 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/ckanext/d4science/views.py @@ -0,0 +1,17 @@ +from flask import Blueprint + + +d4science = Blueprint( + "d4science", __name__) + + +def page(): + return "Hello, d4science!" + + +d4science.add_url_rule( + "/d4science/page", view_func=page) + + +def get_blueprints(): + return [d4science] diff --git a/compose/2.10/src/ckanext-d4science/dev-requirements.txt b/compose/2.10/src/ckanext-d4science/dev-requirements.txt new file mode 100644 index 0000000..eac82b4 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/dev-requirements.txt @@ -0,0 +1 @@ +pytest-ckan diff --git a/compose/2.10/src/ckanext-d4science/requirements.txt b/compose/2.10/src/ckanext-d4science/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science/setup.cfg b/compose/2.10/src/ckanext-d4science/setup.cfg new file mode 100644 index 0000000..94cc26d --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/setup.cfg @@ -0,0 +1,62 @@ +[metadata] +name = ckanext-d4science +version = 0.0.1 +description = d4science custom extensions +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/d4science/ckanext-d4science +author = d4science +author_email = info@d4science.org +license = AGPL +classifiers = + Development Status :: 4 - Beta + License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+) + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 +keywords = CKAN + +[options] +packages = find: +namespace_packages = ckanext +install_requires = +include_package_data = True + +[options.entry_points] +ckan.plugins = + d4science = ckanext.d4science.plugin:D4SciencePlugin + +babel.extractors = + ckan = ckan.lib.extract:extract_ckan + +[options.extras_require] + +[extract_messages] +keywords = translate isPlural +add_comments = TRANSLATORS: +output_file = ckanext/d4science/i18n/ckanext-d4science.pot +width = 80 + +[init_catalog] +domain = ckanext-d4science +input_file = ckanext/d4science/i18n/ckanext-d4science.pot +output_dir = ckanext/d4science/i18n + +[update_catalog] +domain = ckanext-d4science +input_file = ckanext/d4science/i18n/ckanext-d4science.pot +output_dir = ckanext/d4science/i18n +previous = true + +[compile_catalog] +domain = ckanext-d4science +directory = ckanext/d4science/i18n +statistics = true + +[tool:pytest] +filterwarnings = + ignore::sqlalchemy.exc.SADeprecationWarning + ignore::sqlalchemy.exc.SAWarning + ignore::DeprecationWarning +addopts = --ckan-ini test.ini diff --git a/compose/2.10/src/ckanext-d4science/setup.py b/compose/2.10/src/ckanext-d4science/setup.py new file mode 100644 index 0000000..4d26df7 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/setup.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +from setuptools import setup + +setup( + # If you are changing from the default layout of your extension, you may + # have to change the message extractors, you can read more about babel + # message extraction at + # http://babel.pocoo.org/docs/messages/#extraction-method-mapping-and-configuration + message_extractors={ + 'ckanext': [ + ('**.py', 'python', None), + ('**.js', 'javascript', None), + ('**/templates/**.html', 'ckan', None), + ], + } +) diff --git a/compose/2.10/src/ckanext-d4science/test.ini b/compose/2.10/src/ckanext-d4science/test.ini new file mode 100644 index 0000000..67f9d4b --- /dev/null +++ b/compose/2.10/src/ckanext-d4science/test.ini @@ -0,0 +1,45 @@ +[DEFAULT] +debug = false +smtp_server = localhost +error_email_from = ckan@localhost + +[app:main] +use = config:../../src/ckan/test-core.ini + +# Insert any custom config settings to be used when running your extension's +# tests here. These will override the one defined in CKAN core's test-core.ini +ckan.plugins = d4science + + +# Logging configuration +[loggers] +keys = root, ckan, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_ckan] +qualname = ckan +handlers = +level = INFO + +[logger_sqlalchemy] +handlers = +qualname = sqlalchemy.engine +level = WARN + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s diff --git a/compose/2.10/src/ckanext-d4science_theme/.project b/compose/2.10/src/ckanext-d4science_theme/.project new file mode 100644 index 0000000..2c4c958 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/.project @@ -0,0 +1,17 @@ + + + ckanext-d4science_theme + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/compose/2.10/src/ckanext-d4science_theme/.settings/org.eclipse.core.resources.prefs b/compose/2.10/src/ckanext-d4science_theme/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..77f221a --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/setup.py=utf-8 diff --git a/compose/2.10/src/ckanext-d4science_theme/LICENSE b/compose/2.10/src/ckanext-d4science_theme/LICENSE new file mode 100644 index 0000000..3ffc567 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/LICENSE @@ -0,0 +1,661 @@ +GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/MANIFEST.in b/compose/2.10/src/ckanext-d4science_theme/MANIFEST.in new file mode 100644 index 0000000..06e4a8a --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/MANIFEST.in @@ -0,0 +1,2 @@ +include README.rst +recursive-include ckanext/d4science_theme *.html *.json *.js *.less *.css \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/README.rst b/compose/2.10/src/ckanext-d4science_theme/README.rst new file mode 100644 index 0000000..ebc903a --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/README.rst @@ -0,0 +1,129 @@ + +============ +ckanext-d4science_theme +============ + +The CKAN extension that implements the D4Science theme template used by D4Science Catalogues + + +------------ +Requirements +------------ + +None + +------------ +Installation +------------ + +To install ckanext-d4science_theme: + +1. Activate your CKAN virtual environment, for example:: + + . /usr/lib/ckan/default/bin/activate + +2. Install the ckanext-d4science_theme Python package into your virtual environment:: + + pip install ckanext-d4science_theme + +3. Add ``d4science_theme`` to the ``ckan.plugins`` setting in your CKAN + config file (by default the config file is located at + ``/etc/ckan/default/production.ini``). + +4. Restart CKAN. For example if you've deployed CKAN with Apache on Ubuntu:: + + sudo service apache2 reload + + +--------------- +Config Settings +--------------- + +Document any optional config settings here. For example:: + + # The minimum number of hours to wait before re-checking a resource + # (optional, default: 24). + ckanext.d4science_theme.some_setting = some_default_value + + +------------------------ +Development Installation +------------------------ + +To install ckanext-d4science_theme for development, activate your CKAN virtualenv and +do:: + + git clone https://code-repo.d4science.org/CKAN-Extensions/ckanext-d4science_theme.git + cd ckanext-d4science_theme + python setup.py develop + pip install -r dev-requirements.txt + + +----------------- +Running the Tests +----------------- + +To run the tests, do:: + + nosetests --nologcapture --with-pylons=test.ini + +To run the tests and produce a coverage report, first make sure you have +coverage installed in your virtualenv (``pip install coverage``) then run:: + + nosetests --nologcapture --with-pylons=test.ini --with-coverage --cover-package=ckanext.d4science_theme --cover-inclusive --cover-erase --cover-tests + + +--------------------------------- +Registering ckanext-d4science_theme on PyPI +--------------------------------- + +ckanext-d4science_theme should be availabe on PyPI as +https://pypi.python.org/pypi/ckanext-d4science_theme. If that link doesn't work, then +you can register the project on PyPI for the first time by following these +steps: + +1. Create a source distribution of the project:: + + python setup.py sdist + +2. Register the project:: + + python setup.py register + +3. Upload the source distribution to PyPI:: + + python setup.py sdist upload + +4. Tag the first release of the project on GitHub with the version number from + the ``setup.py`` file. For example if the version number in ``setup.py`` is + 0.0.1 then do:: + + git tag 0.0.1 + git push --tags + + +---------------------------------------- +Releasing a New Version of ckanext-d4science_theme +---------------------------------------- + +ckanext-d4science_theme is availabe on PyPI as https://pypi.python.org/pypi/ckanext-d4science_theme. +To publish a new version to PyPI follow these steps: + +1. Update the version number in the ``setup.py`` file. + See `PEP 440 `_ + for how to choose version numbers. + +2. Create a source distribution of the new version:: + + python setup.py sdist + +3. Upload the source distribution to PyPI:: + + python setup.py sdist upload + +4. Tag the new release of the project on GitHub with the version number from + the ``setup.py`` file. For example if the version number in ``setup.py`` is + 0.0.2 then do:: + + git tag 0.0.2 + git push --tags diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/__init__.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/__init__.py new file mode 100644 index 0000000..2e2033b --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/__init__.py @@ -0,0 +1,7 @@ +# this is a namespace package +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + import pkgutil + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/__init__.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity-stream.js b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity-stream.js new file mode 100644 index 0000000..69874ba --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity-stream.js @@ -0,0 +1,34 @@ +/* Activity stream + * Handle the pagination for activity list + * + * Options + * - page: current page number + */ +this.ckan.module('activity-stream', function($) { + return { + + /* Initialises the module setting up elements and event listeners. + * + * Returns nothing. + */ + initialize: function () { + $('#activity_types_filter_select').on( + 'change', + this._onChangeActivityType + ); + }, + + + /* Filter using the selected + * activity type + * + * Returns nothing + */ + _onChangeActivityType: function (event) { + // event.preventDefault(); + url = $("#activity_types_filter_select option:selected" ).data('url'); + window.location = url; + }, + + }; +}); diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity.css b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity.css new file mode 100644 index 0000000..e24a4ae --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity.css @@ -0,0 +1,103 @@ +.activity { + padding: 0; + list-style-type: none; + background: transparent url("/dotted.png") 21px 0 repeat-y; } + .activity .item { + position: relative; + margin: 0 0 15px 0; + padding: 0; } + .activity .item .user-image { + border-radius: 100px; } + .activity .item .date { + color: #999; + font-size: 12px; + white-space: nowrap; + margin: 5px 0 0 80px; } + .activity .item.no-avatar p { + margin-left: 40px; } + +.activity_buttons > a.btn.disabled { + color: inherit; + opacity: 0.70 !important; } + +.popover { + width: 300px; } + .popover .popover-title { + font-weight: bold; + margin-bottom: 0; } + .popover p.about { + margin: 0 0 10px 0; } + .popover .popover-close { + float: right; + text-decoration: none; } + .popover .empty { + padding: 10px; + color: #6e6e6e; + font-style: italic; } + +.activity .item .icon { + color: #999999; } +.activity .item.failure .icon { + color: #B95252; } +.activity .item.success .icon { + color: #69A67A; } +.activity .item.added-tag .icon { + color: #6995a6; } +.activity .item.changed-group .icon { + color: #767DCE; } +.activity .item.changed-package .icon { + color: #8c76ce; } +.activity .item.changed-package_extra .icon { + color: #769ace; } +.activity .item.changed-resource .icon { + color: #aa76ce; } +.activity .item.changed-user .icon { + color: #76b8ce; } +.activity .item.changed-organization .icon { + color: #699fa6; } +.activity .item.deleted-group .icon { + color: #B95252; } +.activity .item.deleted-package .icon { + color: #b97452; } +.activity .item.deleted-package_extra .icon { + color: #b95274; } +.activity .item.deleted-resource .icon { + color: #b99752; } +.activity .item.deleted-organization .icon { + color: #b95297; } +.activity .item.new-group .icon { + color: #69A67A; } +.activity .item.new-package .icon { + color: #69a68e; } +.activity .item.new-package_extra .icon { + color: #6ca669; } +.activity .item.new-resource .icon { + color: #81a669; } +.activity .item.new-user .icon { + color: #69a6a3; } +.activity .item.new-organization .icon { + color: #81a669; } +.activity .item.removed-tag .icon { + color: #b95297; } +.activity .item.deleted-related-item .icon { + color: #b9b952; } +.activity .item.follow-dataset .icon { + color: #767DCE; } +.activity .item.follow-user .icon { + color: #8c76ce; } +.activity .item.new-related-item .icon { + color: #95a669; } +.activity .item.follow-group .icon { + color: #8ba669; } + +.select-time { + width: 250px; + display: inline; } + +br.line-height2 { + line-height: 2; } + +.pull-right { + float: right; } + +/*# sourceMappingURL=activity.css.map */ diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity.css.map b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity.css.map new file mode 100644 index 0000000..76482e9 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAAA,SAAU;EACN,OAAO,EAAE,CAAC;EACV,eAAe,EAAE,IAAI;EACrB,UAAU,EAAE,8CAA+C;EAC3D,eAAM;IACF,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,UAAU;IAClB,OAAO,EAAE,CAAC;IACV,2BAAY;MACR,aAAa,EAAE,KAAK;IAExB,qBAAM;MACF,KAAK,EAAE,IAAI;MACX,SAAS,EAAE,IAAI;MACf,WAAW,EAAE,MAAM;MACnB,MAAM,EAAE,YAAY;IAExB,2BAAc;MAEV,WAAW,EAAE,IAAI;;AAK7B,kCAAmC;EAC/B,KAAK,EAAE,OAAO;EACd,OAAO,EAAE,eAAe;;AAG5B,QAAS;EACL,KAAK,EAAE,KAAK;EACZ,uBAAe;IACX,WAAW,EAAE,IAAI;IACjB,aAAa,EAAE,CAAC;EAEpB,gBAAQ;IACJ,MAAM,EAAE,UAAU;EAEtB,uBAAe;IACX,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,IAAI;EAEzB,eAAO;IACH,OAAO,EAAE,IAAI;IACb,KAAK,EAAE,OAAO;IACd,UAAU,EAAE,MAAM;;AAatB,qBAAM;EACF,KAAK,EARQ,OAAO;AAUxB,6BAAgB;EACZ,KAAK,EAPS,OAAO;AASzB,6BAAgB;EACZ,KAAK,EAbM,OAAO;AAetB,+BAAkB;EACd,KAAK,EAAE,OAAiC;AAE5C,mCAAsB;EAClB,KAAK,EAjBS,OAAO;AAmBzB,qCAAwB;EACpB,KAAK,EAAE,OAAoC;AAE/C,2CAA8B;EAC1B,KAAK,EAAE,OAAqC;AAEhD,sCAAyB;EACrB,KAAK,EAAE,OAAoC;AAE/C,kCAAqB;EACjB,KAAK,EAAE,OAAqC;AAEhD,0CAA6B;EACzB,KAAK,EAAE,OAAiC;AAE5C,mCAAsB;EAClB,KAAK,EAlCS,OAAO;AAoCzB,qCAAwB;EACpB,KAAK,EAAE,OAAoC;AAE/C,2CAA8B;EAC1B,KAAK,EAAE,OAAqC;AAEhD,sCAAyB;EACrB,KAAK,EAAE,OAAoC;AAE/C,0CAA6B;EACzB,KAAK,EAAE,OAAqC;AAEhD,+BAAkB;EACd,KAAK,EApDM,OAAO;AAsDtB,iCAAoB;EAChB,KAAK,EAAE,OAAiC;AAE5C,uCAA0B;EACtB,KAAK,EAAE,OAAkC;AAE7C,kCAAqB;EACjB,KAAK,EAAE,OAAkC;AAE7C,8BAAiB;EACb,KAAK,EAAE,OAAiC;AAE5C,sCAAyB;EACrB,KAAK,EAAE,OAAkC;AAE7C,iCAAoB;EAChB,KAAK,EAAE,OAAqC;AAEhD,0CAA6B;EACzB,KAAK,EAAE,OAAoC;AAE/C,oCAAuB;EACnB,KAAK,EA3EU,OAAO;AA6E1B,iCAAoB;EAChB,KAAK,EAAE,OAAqC;AAEhD,sCAAyB;EACrB,KAAK,EAAE,OAAkC;AAE7C,kCAAqB;EACjB,KAAK,EAAE,OAAkC;;AAIjD,YAAa;EACX,KAAK,EAAE,KAAK;EACZ,OAAO,EAAE,MAAM;;AAGjB,eAAgB;EACd,WAAW,EAAE,CAAC;;AAGhB,WAAY;EACR,KAAK,EAAE,KAAK", +"sources": ["activity.scss"], +"names": [], +"file": "activity.css" +} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity.scss b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity.scss new file mode 100644 index 0000000..1cf52dc --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/activity.scss @@ -0,0 +1,154 @@ +.activity { + padding: 0; + list-style-type: none; + background: transparent url('/dotted.png') 21px 0 repeat-y; + .item { + position: relative; + margin: 0 0 15px 0; + padding: 0; + .user-image { + border-radius: 100px; + } + .date { + color: #999; + font-size: 12px; + white-space: nowrap; + margin: 5px 0 0 80px; + } + &.no-avatar p { + // Use by datapusher + margin-left: 40px; + } + } +} + +.activity_buttons > a.btn.disabled { + color: inherit; + opacity: 0.70 !important; +} + +// For profile information that appears in the popover +.popover { + width: 300px; + .popover-title { + font-weight: bold; + margin-bottom: 0; + } + p.about { + margin: 0 0 10px 0; + } + .popover-close { + float: right; + text-decoration: none; + } + .empty { + padding: 10px; + color: #6e6e6e; + font-style: italic; + } +} + +// Activity Stream base colors +$activityColorText: #FFFFFF; +$activityColorBlank: #999999; +$activityColorNew: #69A67A; +$activityColorNeutral: #767DCE; +$activityColorModify: #767DCE; +$activityColorDelete: #B95252; + +.activity .item { + .icon { + color: $activityColorBlank ; + } // Non defined + &.failure .icon { + color: $activityColorDelete; + } + &.success .icon { + color: $activityColorNew; + } + &.added-tag .icon { + color: adjust-hue($activityColorNew, 60); + } + &.changed-group .icon { + color: $activityColorModify; + } + &.changed-package .icon { + color: adjust-hue($activityColorModify, 20); + } + &.changed-package_extra .icon { + color: adjust-hue($activityColorModify, -20); + } + &.changed-resource .icon { + color: adjust-hue($activityColorModify, 40); + } + &.changed-user .icon { + color: adjust-hue($activityColorModify, -40); + } + &.changed-organization .icon { + color: adjust-hue($activityColorNew, 50); + } + &.deleted-group .icon { + color: $activityColorDelete; + } + &.deleted-package .icon { + color: adjust-hue($activityColorDelete, 20); + } + &.deleted-package_extra .icon { + color: adjust-hue($activityColorDelete, -20); + } + &.deleted-resource .icon { + color: adjust-hue($activityColorDelete, 40); + } + &.deleted-organization .icon { + color: adjust-hue($activityColorDelete, -40); + } + &.new-group .icon { + color: $activityColorNew; + } + &.new-package .icon { + color: adjust-hue($activityColorNew, 20); + } + &.new-package_extra .icon { + color: adjust-hue($activityColorNew, -20); + } + &.new-resource .icon { + color: adjust-hue($activityColorNew, -40); + } + &.new-user .icon { + color: adjust-hue($activityColorNew, 40); + } + &.new-organization .icon { + color: adjust-hue($activityColorNew, -40); + } + &.removed-tag .icon { + color: adjust-hue($activityColorDelete, -40); + } + &.deleted-related-item .icon { + color: adjust-hue($activityColorDelete, 60); + } + &.follow-dataset .icon { + color: $activityColorNeutral; + } + &.follow-user .icon { + color: adjust-hue($activityColorNeutral, 20); + } + &.new-related-item .icon { + color: adjust-hue($activityColorNew, -60); + } + &.follow-group .icon { + color: adjust-hue($activityColorNew, -50); + } +} + +.select-time { + width: 250px; + display: inline; +} + +br.line-height2 { + line-height: 2; +} + +.pull-right { + float: right; +} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/dashboard.js b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/dashboard.js new file mode 100644 index 0000000..69b97c7 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/dashboard.js @@ -0,0 +1,95 @@ +/* User Dashboard + * Handles the filter dropdown menu and the reduction of the notifications number + * within the header to zero + * + * Examples + * + *
+ * + */ +this.ckan.module('dashboard', function ($) { + return { + button: null, + popover: null, + searchTimeout: null, + + /* Initialises the module setting up elements and event listeners. + * + * Returns nothing. + */ + initialize: function () { + $.proxyAll(this, /_on/); + this.button = $('#followee-filter .btn'). + on('click', this._onShowFolloweeDropdown); + var title = this.button.prop('title'); + + this.button.popover = new bootstrap.Popover(document.querySelector('#followee-filter .btn'), { + placement: 'bottom', + html: true, + template: '', + customClass: 'popover-followee', + sanitizeFn: function (content) { + return DOMPurify.sanitize(content, { ALLOWED_TAGS: [ + "form", "div", "input", "footer", "header", "h1", "h2", "h3", "h4", + "small", "span", "strong", "i", 'a', 'li', 'ul','p' + + ]}); + }, + content: $('#followee-content').html() + }); + this.button.prop('title', title); + }, + + /* Handles click event on the 'show me:' dropdown button + * + * Returns nothing. + */ + _onShowFolloweeDropdown: function() { + this.button.toggleClass('active'); + if (this.button.hasClass('active')) { + setTimeout(this._onInitSearch, 100); + } + return false; + }, + + /* Handles focusing on the input and making sure that the keyup + * even is applied to the input + * + * Returns nothing. + */ + _onInitSearch: function() { + var input = $('input', this.popover); + if (!input.hasClass('inited')) { + input. + on('keyup', this._onSearchKeyUp). + addClass('inited'); + } + input.focus(); + }, + + /* Handles the keyup event + * + * Returns nothing. + */ + _onSearchKeyUp: function() { + clearTimeout(this.searchTimeout); + this.searchTimeout = setTimeout(this._onSearchKeyUpTimeout, 300); + }, + + /* Handles the actual filtering of search results + * + * Returns nothing. + */ + _onSearchKeyUpTimeout: function() { + this.popover = this.button.popover.tip; + var input = $('input', this.popover); + var q = input.val().toLowerCase(); + if (q) { + $('li', this.popover).hide(); + $('li.everything, [data-search^="' + q + '"]', this.popover).show(); + } else { + $('li', this.popover).show(); + } + } + }; +}); diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/dashboard.spec.js b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/dashboard.spec.js new file mode 100644 index 0000000..95ef2f2 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/dashboard.spec.js @@ -0,0 +1,64 @@ +describe('ckan.modules.DashboardModule()', function () { + before(() => { + cy.visit('/'); + cy.window().then(win => { + cy.wrap(win.ckan.module.registry['dashboard']).as('dashboard'); + win.jQuery('
').appendTo(win.document.body) + cy.loadFixture('dashboard.html').then((template) => { + cy.wrap(template).as('template'); + }); + }) + }); + + beforeEach(function () { + cy.window().then(win => { + win.jQuery('#fixture').html(this.template); + this.el = document.createElement('button'); + this.sandbox = win.ckan.sandbox(); + this.sandbox.body = win.jQuery('#fixture'); + cy.wrap(this.sandbox.body).as('fixture'); + this.module = new this.dashboard(this.el, {}, this.sandbox); + }) + }); + + afterEach(function () { + //this.fixture.empty(); + }); + + describe('.initialize()', function () { + it('should bind callback methods to the module', function () { + cy.window().then(win => { + let target = cy.stub(win.jQuery, 'proxyAll'); + + this.module.initialize(); + + expect(target).to.be.called; + expect(target).to.be.calledWith(this.module, /_on/); + + target.restore(); + }) + }) + }) + + describe('.show()', function () { + it('should append the popover to the document body', function () { + this.module.initialize(); + this.module.button.click(); + assert.equal(this.fixture.children().length, 1); + assert.equal(this.fixture.find('#followee-filter').length, 1); + assert.equal(this.fixture.find('#followee-filter .input-group input').length, 1); + }); + }) + + describe(".search", function(){ + it('should filter based on query', function() { + this.module.initialize(); + this.module.button.click(); + cy.get('#fixture #followee-filter #followee-content').invoke('removeAttr', 'style'); + cy.get('#fixture #followee-filter .nav li').should('have.length', 3); + cy.get('#fixture #followee-filter .input-group input.inited').type('text'); + cy.get('#fixture #followee-filter .nav li[data-search="not valid"]').should('be.visible'); + cy.get('#fixture #followee-filter .nav li[data-search="test followee"]').should('be.visible'); + }) + }) +}) diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/webassets.yml b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/webassets.yml new file mode 100644 index 0000000..e762585 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/assets/webassets.yml @@ -0,0 +1,14 @@ +activity: + filters: rjsmin + output: activity/%(version)s_activity.js + extra: + preload: + - base/main + contents: + - dashboard.js + - activity-stream.js + +activity-css: + output: ckanext-activity/%(version)s_activity.css + contents: + - activity.css diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/changes.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/changes.py new file mode 100644 index 0000000..81f090f --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/changes.py @@ -0,0 +1,1255 @@ +# encoding: utf-8 +""" +Functions for generating a list of differences between two versions of a +dataset +""" +from __future__ import annotations + +import logging +from typing import Any +from typing_extensions import TypeAlias, TypedDict + +log = logging.getLogger(__name__) + +Data: TypeAlias = "dict[str, Any]" +ChangeList: TypeAlias = "list[Data]" + + +class Extra(TypedDict): + key: str + value: Any + + +def _extras_to_dict(extras_list: list[Extra]) -> Data: + """ + Takes a list of dictionaries with the following format: + [ + { + "key": , + "value": + }, + ..., + { + "key": , + "value": + } + ] + and converts it into a single dictionary with the following + format: + { + key_0: value_0, + ..., + key_n: value_n + + } + """ + ret_dict = {} + # the extras_list is a list of dictionaries + for dict in extras_list: + ret_dict[dict["key"]] = dict["value"] + + return ret_dict + + +def check_resource_changes( + change_list: ChangeList, old: Data, new: Data, old_activity_id: str +) -> None: + """ + Compares two versions of a dataset and records the changes between them + (just the resources) in change_list. e.g. resources that are added, changed + or deleted. For existing resources, checks whether their names, formats, + and/or descriptions have changed, as well as whether the url changed (e.g. + a new file has been uploaded for the resource). + """ + + # list of default fields in a resource's metadata dictionary - used + # later to ensure that we don't count changes to default fields as changes + # to extra fields + fields = [ + "package_id", + "url", + "revision_id", + "description", + "format", + "hash", + "name", + "resource_type", + "mimetype", + "mimetype_inner", + "cache_url", + "size", + "created", + "last_modified", + "metadata_modified", + "cache_last_updated", + "upload", + "position", + ] + default_fields_set = set(fields) + + # make a set of the resource IDs present in old and new + old_resource_set = set() + old_resource_dict = {} + new_resource_set = set() + new_resource_dict = {} + + for resource in old.get("resources", []): + old_resource_set.add(resource["id"]) + old_resource_dict[resource["id"]] = { + key: value for (key, value) in resource.items() if key != "id" + } + + for resource in new.get("resources", []): + new_resource_set.add(resource["id"]) + new_resource_dict[resource["id"]] = { + key: value for (key, value) in resource.items() if key != "id" + } + + # get the IDs of the resources that have been added between the versions + new_resources = list(new_resource_set - old_resource_set) + for resource_id in new_resources: + change_list.append( + { + "type": "new_resource", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_name": new_resource_dict[resource_id].get("name"), + "resource_id": resource_id, + } + ) + + # get the IDs of resources that have been deleted between versions + deleted_resources = list(old_resource_set - new_resource_set) + for resource_id in deleted_resources: + change_list.append( + { + "type": "delete_resource", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": old_resource_dict[resource_id].get("name"), + "old_activity_id": old_activity_id, + } + ) + + # now check the resources that are in both and see if any + # have been changed + resources = new_resource_set.intersection(old_resource_set) + for resource_id in resources: + old_metadata = old_resource_dict[resource_id] + new_metadata = new_resource_dict[resource_id] + + if old_metadata.get("name") != new_metadata.get("name"): + change_list.append( + { + "type": "resource_name", + "title": new.get("title"), + "old_pkg_id": old["id"], + "new_pkg_id": new["id"], + "resource_id": resource_id, + "old_resource_name": old_resource_dict[resource_id].get( + "name" + ), + "new_resource_name": new_resource_dict[resource_id].get( + "name" + ), + "old_activity_id": old_activity_id, + } + ) + + # you can't remove a format, but if a resource's format isn't + # recognized, it won't have one set + + # if a format was not originally set and the user set one + if not old_metadata.get("format") and new_metadata.get("format"): + change_list.append( + { + "type": "resource_format", + "method": "add", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_resource_dict[resource_id].get( + "name" + ), + "org_id": new["organization"]["id"] + if new.get("organization") + else "", + "format": new_metadata.get("format"), + } + ) + + # if both versions have a format but the format changed + elif old_metadata.get("format") != new_metadata.get("format"): + change_list.append( + { + "type": "resource_format", + "method": "change", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_resource_dict[resource_id].get( + "name" + ), + "org_id": new["organization"]["id"] + if new.get("organization") + else "", + "old_format": old_metadata.get("format"), + "new_format": new_metadata.get("format"), + } + ) + + # if the description changed + if not old_metadata.get("description") and new_metadata.get( + "description" + ): + change_list.append( + { + "type": "resource_desc", + "method": "add", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_resource_dict[resource_id].get( + "name" + ), + "new_desc": new_metadata.get("description"), + } + ) + + # if there was a description but the user removed it + elif old_metadata.get("description") and not new_metadata.get( + "description" + ): + change_list.append( + { + "type": "resource_desc", + "method": "remove", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_resource_dict[resource_id].get( + "name" + ), + } + ) + + # if both have descriptions but they are different + elif old_metadata.get("description") != new_metadata.get( + "description" + ): + change_list.append( + { + "type": "resource_desc", + "method": "change", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_resource_dict[resource_id].get( + "name" + ), + "new_desc": new_metadata.get("description"), + "old_desc": old_metadata.get("description"), + } + ) + + # check if the url changes (e.g. user uploaded a new file) + # TODO: use regular expressions to determine the actual name of the + # new and old files + if old_metadata.get("url") != new_metadata.get("url"): + change_list.append( + { + "type": "new_file", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_metadata.get("name"), + } + ) + + # check any extra fields in the resource + # remove default fields from these sets to make sure we only check + # for changes to extra fields + old_fields_set = set(old_metadata.keys()) + old_fields_set = old_fields_set - default_fields_set + new_fields_set = set(new_metadata.keys()) + new_fields_set = new_fields_set - default_fields_set + + # determine if any new extra fields have been added + new_fields = list(new_fields_set - old_fields_set) + if len(new_fields) == 1: + if new_metadata[new_fields[0]]: + change_list.append( + { + "type": "resource_extras", + "method": "add_one_value", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_metadata.get("name"), + "key": new_fields[0], + "value": new_metadata[new_fields[0]], + } + ) + else: + change_list.append( + { + "type": "resource_extras", + "method": "add_one_no_value", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_metadata.get("name"), + "key": new_fields[0], + } + ) + elif len(new_fields) > 1: + change_list.append( + { + "type": "resource_extras", + "method": "add_multiple", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_metadata.get("name"), + "key_list": new_fields, + "value_list": [ + new_metadata[field] for field in new_fields + ], + } + ) + + # determine if any extra fields have been removed + deleted_fields = list(old_fields_set - new_fields_set) + if len(deleted_fields) == 1: + change_list.append( + { + "type": "resource_extras", + "method": "remove_one", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_metadata.get("name"), + "key": deleted_fields[0], + } + ) + elif len(deleted_fields) > 1: + change_list.append( + { + "type": "resource_extras", + "method": "remove_multiple", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_metadata.get("name"), + "key_list": deleted_fields, + } + ) + + # determine if any extra fields have been changed + # changed_fields is only a set of POTENTIALLY changed fields - we + # still have to check if any of the values associated with the fields + # have actually changed + changed_fields = list(new_fields_set.intersection(old_fields_set)) + for field in changed_fields: + if new_metadata[field] != old_metadata[field]: + if new_metadata[field] and old_metadata[field]: + change_list.append( + { + "type": "resource_extras", + "method": "change_value_with_old", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_metadata.get("name"), + "key": field, + "old_value": old_metadata[field], + "new_value": new_metadata[field], + } + ) + elif not old_metadata[field]: + change_list.append( + { + "type": "resource_extras", + "method": "change_value_no_old", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_metadata.get("name"), + "key": field, + "new_value": new_metadata[field], + } + ) + elif not new_metadata[field]: + change_list.append( + { + "type": "resource_extras", + "method": "change_value_no_new", + "pkg_id": new["id"], + "title": new.get("title"), + "resource_id": resource_id, + "resource_name": new_metadata.get("name"), + "key": field, + } + ) + + +def check_metadata_changes( + change_list: ChangeList, old: Data, new: Data +) -> None: + """ + Compares two versions of a dataset and records the changes between them + (excluding resources) in change_list. + """ + # if the title has changed + if old.get("title") != new.get("title"): + _title_change(change_list, old, new) + + # if the owner organization changed + if old.get("owner_org") != new.get("owner_org"): + _org_change(change_list, old, new) + + # if the maintainer of the dataset changed + if old.get("maintainer") != new.get("maintainer"): + _maintainer_change(change_list, old, new) + + # if the maintainer email of the dataset changed + if old.get("maintainer_email") != new.get("maintainer_email"): + _maintainer_email_change(change_list, old, new) + + # if the author of the dataset changed + if old.get("author") != new.get("author"): + _author_change(change_list, old, new) + + # if the author email of the dataset changed + if old.get("author_email") != new.get("author_email"): + _author_email_change(change_list, old, new) + + # if the visibility of the dataset changed + if old.get("private") != new.get("private"): + change_list.append( + { + "type": "private", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new": "Private" if bool(new.get("private")) else "Public", + } + ) + + # if the description of the dataset changed + if old.get("notes") != new.get("notes"): + _notes_change(change_list, old, new) + + # make sets out of the tags for each dataset + old_tags = {tag.get("name") for tag in old.get("tags", [])} + new_tags = {tag.get("name") for tag in new.get("tags", [])} + # if the tags have changed + if old_tags != new_tags: + _tag_change(change_list, new_tags, old_tags, new) + + # if the license has changed + if old.get("license_title") != new.get("license_title"): + _license_change(change_list, old, new) + + # if the name of the dataset has changed + # this is only visible to the user via the dataset's URL, + # so display the change using that + if old.get("name") != new.get("name"): + _name_change(change_list, old, new) + + # if the source URL (metadata value, not the actual URL of the dataset) + # has changed + if old.get("url") != new.get("url"): + _url_change(change_list, old, new) + + # if the user-provided version has changed + if old.get("version") != new.get("version"): + _version_change(change_list, old, new) + + # check whether fields added by extensions or custom fields + # (in the "extras" field) have been changed + + _extension_fields(change_list, old, new) + _extra_fields(change_list, old, new) + + +def check_metadata_org_changes(change_list: ChangeList, old: Data, new: Data): + """ + Compares two versions of a organization and records the changes between + them in change_list. + """ + # if the title has changed + if old.get("title") != new.get("title"): + _title_change(change_list, old, new) + + # if the description of the organization changed + if old.get("description") != new.get("description"): + _description_change(change_list, old, new) + + # if the image URL has changed + if old.get("image_url") != new.get("image_url"): + _image_url_change(change_list, old, new) + + +def _title_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a dataset's title between two versions + (old and new) to change_list. + """ + change_list.append( + { + "type": "title", + "id": new.get("name"), + "new_title": new.get("title"), + "old_title": old.get("title"), + } + ) + + +def _org_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a dataset's organization between + two versions (old and new) to change_list. + """ + + # if both versions belong to an organization + if old.get("owner_org") and new.get("owner_org"): + change_list.append( + { + "type": "org", + "method": "change", + "pkg_id": new.get("id"), + "title": new.get("title"), + "old_org_id": old["organization"].get("id"), + "old_org_title": old["organization"].get("title"), + "new_org_id": new["organization"].get("id"), + "new_org_title": new["organization"].get("title"), + } + ) + # if the dataset was not in an organization before and it is now + elif not old.get("owner_org") and new.get("owner_org"): + change_list.append( + { + "type": "org", + "method": "add", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_org_id": new["organization"].get("id"), + "new_org_title": new["organization"].get("title"), + } + ) + # if the user removed the organization + else: + change_list.append( + { + "type": "org", + "method": "remove", + "pkg_id": new.get("id"), + "title": new.get("title"), + "old_org_id": old["organization"].get("id"), + "old_org_title": old["organization"].get("title"), + } + ) + + +def _maintainer_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a dataset's maintainer field between two + versions (old and new) to change_list. + """ + # if the old dataset had a maintainer + if old.get("maintainer") and new.get("maintainer"): + change_list.append( + { + "type": "maintainer", + "method": "change", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_maintainer": new["maintainer"], + "old_maintainer": old["maintainer"], + } + ) + # if they removed the maintainer + elif not new.get("maintainer"): + change_list.append( + { + "type": "maintainer", + "pkg_id": new.get("id"), + "title": new.get("title"), + "method": "remove", + } + ) + # if there wasn't one there before + else: + change_list.append( + { + "type": "maintainer", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_maintainer": new.get("maintainer"), + "method": "add", + } + ) + + +def _maintainer_email_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a dataset's maintainer e-mail address + field between two versions (old and new) to change_list. + """ + # if the old dataset had a maintainer email + if old.get("maintainer_email") and new.get("maintainer_email"): + change_list.append( + { + "type": "maintainer_email", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_maintainer_email": new.get("maintainer_email"), + "old_maintainer_email": old.get("maintainer_email"), + "method": "change", + } + ) + # if they removed the maintainer email + elif not new.get("maintainer_email"): + change_list.append( + { + "type": "maintainer_email", + "pkg_id": new.get("id"), + "title": new.get("title"), + "method": "remove", + } + ) + # if there wasn't one there before e + else: + change_list.append( + { + "type": "maintainer_email", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_maintainer_email": new.get("maintainer_email"), + "method": "add", + } + ) + + +def _author_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a dataset's author field between two + versions (old and new) to change_list. + """ + # if the old dataset had an author + if old.get("author") and new.get("author"): + change_list.append( + { + "type": "author", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_author": new.get("author"), + "old_author": old.get("author"), + "method": "change", + } + ) + # if they removed the author + elif not new.get("author"): + change_list.append( + { + "type": "author", + "pkg_id": new.get("id"), + "title": new.get("title"), + "method": "remove", + } + ) + # if there wasn't one there before + else: + change_list.append( + { + "type": "author", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_author": new.get("author"), + "method": "add", + } + ) + + +def _author_email_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a dataset's author e-mail address field + between two versions (old and new) to change_list. + """ + if old.get("author_email") and new.get("author_email"): + change_list.append( + { + "type": "author_email", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_author_email": new.get("author_email"), + "old_author_email": old.get("author_email"), + "method": "change", + } + ) + # if they removed the author + elif not new.get("author_email"): + change_list.append( + { + "type": "author_email", + "pkg_id": new.get("id"), + "title": new.get("title"), + "method": "remove", + } + ) + # if there wasn't one there before + else: + change_list.append( + { + "type": "author_email", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_author_email": new.get("author_email"), + "method": "add", + } + ) + + +def _notes_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a dataset's description between two + versions (old and new) to change_list. + """ + # if the old dataset had a description + if old.get("notes") and new.get("notes"): + change_list.append( + { + "type": "notes", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_notes": new.get("notes"), + "old_notes": old.get("notes"), + "method": "change", + } + ) + elif not new.get("notes"): + change_list.append( + { + "type": "notes", + "pkg_id": new.get("id"), + "title": new.get("title"), + "method": "remove", + } + ) + else: + change_list.append( + { + "type": "notes", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_notes": new.get("notes"), + "method": "add", + } + ) + + +def _tag_change( + change_list: ChangeList, new_tags: set[Any], old_tags: set[Any], new: Data +): + """ + Appends a summary of a change to a dataset's tag list between two + versions (old and new) to change_list. + """ + deleted_tags = old_tags - new_tags + deleted_tags_list = list(deleted_tags) + if len(deleted_tags) == 1: + change_list.append( + { + "type": "tags", + "method": "remove_one", + "pkg_id": new.get("id"), + "title": new.get("title"), + "tag": deleted_tags_list[0], + } + ) + elif len(deleted_tags) > 1: + change_list.append( + { + "type": "tags", + "method": "remove_multiple", + "pkg_id": new.get("id"), + "title": new.get("title"), + "tags": deleted_tags_list, + } + ) + + added_tags = new_tags - old_tags + added_tags_list = list(added_tags) + if len(added_tags) == 1: + change_list.append( + { + "type": "tags", + "method": "add_one", + "pkg_id": new.get("id"), + "title": new.get("title"), + "tag": added_tags_list[0], + } + ) + elif len(added_tags) > 1: + change_list.append( + { + "type": "tags", + "method": "add_multiple", + "pkg_id": new.get("id"), + "title": new.get("title"), + "tags": added_tags_list, + } + ) + + +def _license_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a dataset's license between two versions + (old and new) to change_list. + """ + old_license_url = "" + new_license_url = "" + # if the license has a URL + if "license_url" in old and old["license_url"]: + old_license_url = old["license_url"] + if "license_url" in new and new["license_url"]: + new_license_url = new["license_url"] + change_list.append( + { + "type": "license", + "pkg_id": new.get("id"), + "title": new.get("title"), + "old_url": old_license_url, + "new_url": new_license_url, + "new_title": new.get("license_title"), + "old_title": old.get("license_title"), + } + ) + + +def _name_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a dataset's name (and thus the URL it + can be accessed at) between two versions (old and new) to + change_list. + """ + change_list.append( + { + "type": "name", + "pkg_id": new.get("id"), + "title": new.get("title"), + "old_name": old.get("name"), + "new_name": new.get("name"), + } + ) + + +def _url_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a dataset's source URL (metadata field, + not its actual URL in the datahub) between two versions (old and + new) to change_list. + """ + # if both old and new versions have source URLs + if old.get("url") and new.get("url"): + change_list.append( + { + "type": "url", + "method": "change", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_url": new.get("url"), + "old_url": old.get("url"), + } + ) + # if the user removed the source URL + elif not new.get("url"): + change_list.append( + { + "type": "url", + "method": "remove", + "pkg_id": new.get("id"), + "title": new.get("title"), + "old_url": old.get("url"), + } + ) + # if there wasn't one there before + else: + change_list.append( + { + "type": "url", + "method": "add", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_url": new.get("url"), + } + ) + + +def _version_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a dataset's version field (inputted + by the user, not from version control) between two versions (old + and new) to change_list. + """ + # if both old and new versions have version numbers + if old.get("version") and new.get("version"): + change_list.append( + { + "type": "version", + "method": "change", + "pkg_id": new.get("id"), + "title": new.get("title"), + "old_version": old.get("version"), + "new_version": new.get("version"), + } + ) + # if the user removed the version number + elif not new.get("version"): + change_list.append( + { + "type": "version", + "method": "remove", + "pkg_id": new.get("id"), + "title": new.get("title"), + "old_version": old.get("version"), + } + ) + # if there wasn't one there before + else: + change_list.append( + { + "type": "version", + "method": "add", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_version": new.get("version"), + } + ) + + +def _extension_fields(change_list: ChangeList, old: Data, new: Data): + """ + Checks whether any fields that have been added to the package + dictionaries by CKAN extensions have been changed between versions. + If there have been any changes between the two versions (old and + new), a general summary of the change is appended to change_list. This + function does not produce summaries for fields added or deleted by + extensions, since these changes are not triggered by the user in the web + interface or API. + """ + # list of the default metadata fields for a dataset + # any fields that are not part of this list are custom fields added by a + # user or extension + fields = [ + "owner_org", + "maintainer", + "maintainer_email", + "relationships_as_object", + "private", + "num_tags", + "id", + "metadata_created", + "metadata_modified", + "author", + "author_email", + "state", + "version", + "license_id", + "type", + "resources", + "num_resources", + "tags", + "title", + "groups", + "creator_user_id", + "relationships_as_subject", + "name", + "isopen", + "url", + "notes", + "license_title", + "extras", + "license_url", + "organization", + "revision_id", + ] + fields_set = set(fields) + + # if there are any fields from extensions that are in the new dataset and + # have been updated, print a generic message stating that + old_set = set(old.keys()) + new_set = set(new.keys()) + + # set of additional fields in the new dictionary + addl_fields_new = new_set - fields_set + # set of additional fields in the old dictionary + addl_fields_old = old_set - fields_set + # set of additional fields in both + addl_fields = addl_fields_new.intersection(addl_fields_old) + + # do NOT display a change if any additional fields have been + # added or deleted, since that is not a change made by the user + # from the web interface + + # if additional fields have been changed + addl_fields_list = list(addl_fields) + for field in addl_fields_list: + if old.get(field) != new.get(field): + change_list.append( + { + "type": "extension_fields", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key": field, + "value": new.get(field), + } + ) + + +def _extra_fields(change_list: ChangeList, old: Data, new: Data): + """ + Checks whether a user has added, removed, or changed any extra fields + from the web interface (or API?) and appends a summary of each change to + change_list. + """ + if "extras" in new: + extra_fields_new = _extras_to_dict(new.get("extras", [])) + extra_new_set = set(extra_fields_new.keys()) + + # if the old version has extra fields, we need + # to compare the new version's extras to the old ones + if "extras" in old: + extra_fields_old = _extras_to_dict(old.get("extras", [])) + extra_old_set = set(extra_fields_old.keys()) + + # if some fields were added + new_fields = list(extra_new_set - extra_old_set) + if len(new_fields) == 1: + if extra_fields_new[new_fields[0]]: + change_list.append( + { + "type": "extra_fields", + "method": "add_one_value", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key": new_fields[0], + "value": extra_fields_new[new_fields[0]], + } + ) + else: + change_list.append( + { + "type": "extra_fields", + "method": "add_one_no_value", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key": new_fields[0], + } + ) + elif len(new_fields) > 1: + change_list.append( + { + "type": "extra_fields", + "method": "add_multiple", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key_list": new_fields, + "value_list": extra_fields_new, + } + ) + + # if some fields were deleted + deleted_fields = list(extra_old_set - extra_new_set) + if len(deleted_fields) == 1: + change_list.append( + { + "type": "extra_fields", + "method": "remove_one", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key": deleted_fields[0], + } + ) + elif len(deleted_fields) > 1: + change_list.append( + { + "type": "extra_fields", + "method": "remove_multiple", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key_list": deleted_fields, + } + ) + + # if some existing fields were changed + # list of extra fields in both the old and new versions + extra_fields = list(extra_new_set.intersection(extra_old_set)) + for field in extra_fields: + if extra_fields_old[field] != extra_fields_new[field]: + if extra_fields_old[field]: + change_list.append( + { + "type": "extra_fields", + "method": "change_with_old_value", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key": field, + "old_value": extra_fields_old[field], + "new_value": extra_fields_new[field], + } + ) + else: + change_list.append( + { + "type": "extra_fields", + "method": "change_no_old_value", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key": field, + "new_value": extra_fields_new[field], + } + ) + + # if the old version didn't have an extras field, + # the user could only have added a field (not changed or deleted) + else: + new_fields = list(extra_new_set) + if len(new_fields) == 1: + if extra_fields_new[new_fields[0]]: + change_list.append( + { + "type": "extra_fields", + "method": "add_one_value", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key": new_fields[0], + "value": extra_fields_new[new_fields[0]], + } + ) + else: + change_list.append( + { + "type": "extra_fields", + "method": "add_one_no_value", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key": new_fields[0], + } + ) + + elif len(new_fields) > 1: + change_list.append( + { + "type": "extra_fields", + "method": "add_multiple", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key_list": new_fields, + "value_list": extra_fields_new, + } + ) + + elif "extras" in old: + deleted_fields = list(_extras_to_dict(old["extras"]).keys()) + if len(deleted_fields) == 1: + change_list.append( + { + "type": "extra_fields", + "method": "remove_one", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key": deleted_fields[0], + } + ) + elif len(deleted_fields) > 1: + change_list.append( + { + "type": "extra_fields", + "method": "remove_multiple", + "pkg_id": new.get("id"), + "title": new.get("title"), + "key_list": deleted_fields, + } + ) + + +def _description_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a organization's description between two + versions (old and new) to change_list. + """ + + # if the old organization had a description + if old.get("description") and new.get("description"): + change_list.append( + { + "type": "description", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_description": new.get("description"), + "old_description": old.get("description"), + "method": "change", + } + ) + elif not new.get("description"): + change_list.append( + { + "type": "description", + "pkg_id": new.get("id"), + "title": new.get("title"), + "method": "remove", + } + ) + else: + change_list.append( + { + "type": "description", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_description": new.get("description"), + "method": "add", + } + ) + + +def _image_url_change(change_list: ChangeList, old: Data, new: Data): + """ + Appends a summary of a change to a organization's image URL between two + versions (old and new) to change_list. + """ + # if both old and new versions have image URLs + if old.get("image_url") and new.get("image_url"): + change_list.append( + { + "type": "image_url", + "method": "change", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_image_url": new.get("image_url"), + "old_image_url": old.get("image_url"), + } + ) + # if the user removed the image URL + elif not new.get("image_url"): + change_list.append( + { + "type": "image_url", + "method": "remove", + "pkg_id": new.get("id"), + "title": new.get("title"), + "old_image_url": old.get("image_url"), + } + ) + # if there wasn't one there before + else: + change_list.append( + { + "type": "image_url", + "method": "add", + "pkg_id": new.get("id"), + "title": new.get("title"), + "new_image_url": new.get("image_url"), + } + ) diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/email_notifications.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/email_notifications.py new file mode 100644 index 0000000..d7c49d2 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/email_notifications.py @@ -0,0 +1,278 @@ +# encoding: utf-8 + +""" +Code for generating email notifications for users (e.g. email notifications for +new activities in your dashboard activity stream) and emailing them to the +users. + +""" +from __future__ import annotations + +import datetime +import re +from typing import Any, cast +from jinja2 import Environment + +import ckan.model as model +import ckan.logic as logic +import ckan.lib.jinja_extensions as jinja_extensions + +from ckan.common import ungettext, ugettext, config +from ckan.types import Context + + +def string_to_timedelta(s: str) -> datetime.timedelta: + """Parse a string s and return a standard datetime.timedelta object. + + Handles days, hours, minutes, seconds, and microseconds. + + Accepts strings in these formats: + + 2 days + 14 days + 4:35:00 (hours, minutes and seconds) + 4:35:12.087465 (hours, minutes, seconds and microseconds) + 7 days, 3:23:34 + 7 days, 3:23:34.087465 + .087465 (microseconds only) + + :raises ckan.logic.ValidationError: if the given string does not match any + of the recognised formats + + """ + patterns = [] + days_only_pattern = r"(?P\d+)\s+day(s)?" + patterns.append(days_only_pattern) + hms_only_pattern = r"(?P\d?\d):(?P\d\d):(?P\d\d)" + patterns.append(hms_only_pattern) + ms_only_pattern = r".(?P\d\d\d)(?P\d\d\d)" + patterns.append(ms_only_pattern) + hms_and_ms_pattern = hms_only_pattern + ms_only_pattern + patterns.append(hms_and_ms_pattern) + days_and_hms_pattern = r"{0},\s+{1}".format( + days_only_pattern, hms_only_pattern + ) + patterns.append(days_and_hms_pattern) + days_and_hms_and_ms_pattern = days_and_hms_pattern + ms_only_pattern + patterns.append(days_and_hms_and_ms_pattern) + + match = None + for pattern in patterns: + match = re.match("^{0}$".format(pattern), s) + if match: + break + + if not match: + raise logic.ValidationError( + {"message": "Not a valid time: {0}".format(s)} + ) + + gd = match.groupdict() + days = int(gd.get("days", "0")) + hours = int(gd.get("hours", "0")) + minutes = int(gd.get("minutes", "0")) + seconds = int(gd.get("seconds", "0")) + milliseconds = int(gd.get("milliseconds", "0")) + microseconds = int(gd.get("microseconds", "0")) + delta = datetime.timedelta( + days=days, + hours=hours, + minutes=minutes, + seconds=seconds, + milliseconds=milliseconds, + microseconds=microseconds, + ) + return delta + + +def render_activity_email(activities: list[dict[str, Any]]) -> str: + globals = {"site_title": config.get("ckan.site_title")} + template_name = "activity_streams/activity_stream_email_notifications.text" + + env = Environment(**jinja_extensions.get_jinja_env_options()) + # Install the given gettext, ngettext callables into the environment + env.install_gettext_callables(ugettext, ungettext) # type: ignore + + template = env.get_template(template_name, globals=globals) + return template.render({"activities": activities}) + + +def _notifications_for_activities( + activities: list[dict[str, Any]], user_dict: dict[str, Any] +) -> list[dict[str, str]]: + """Return one or more email notifications covering the given activities. + + This function handles grouping multiple activities into a single digest + email. + + :param activities: the activities to consider + :type activities: list of activity dicts like those returned by + ckan.logic.action.get.dashboard_activity_list() + + :returns: a list of email notifications + :rtype: list of dicts each with keys 'subject' and 'body' + + """ + if not activities: + return [] + + if not user_dict.get("activity_streams_email_notifications"): + return [] + + # We just group all activities into a single "new activity" email that + # doesn't say anything about _what_ new activities they are. + # TODO: Here we could generate some smarter content for the emails e.g. + # say something about the contents of the activities, or single out + # certain types of activity to be sent in their own individual emails, + # etc. + + subject = ungettext( + "{n} new activity from {site_title}", + "{n} new activities from {site_title}", + len(activities), + ).format(site_title=config.get("ckan.site_title"), n=len(activities)) + + body = render_activity_email(activities) + notifications = [{"subject": subject, "body": body}] + + return notifications + + +def _notifications_from_dashboard_activity_list( + user_dict: dict[str, Any], since: datetime.datetime +) -> list[dict[str, str]]: + """Return any email notifications from the given user's dashboard activity + list since `since`. + + """ + # Get the user's dashboard activity stream. + context = cast( + Context, + {"model": model, "session": model.Session, "user": user_dict["id"]}, + ) + activity_list = logic.get_action("dashboard_activity_list")(context, {}) + + # Filter out the user's own activities., so they don't get an email every + # time they themselves do something (we are not Trac). + activity_list = [ + activity + for activity in activity_list + if activity["user_id"] != user_dict["id"] + ] + + # Filter out the old activities. + strptime = datetime.datetime.strptime + fmt = "%Y-%m-%dT%H:%M:%S.%f" + activity_list = [ + activity + for activity in activity_list + if strptime(activity["timestamp"], fmt) > since + ] + + return _notifications_for_activities(activity_list, user_dict) + + +# A list of functions that provide email notifications for users from different +# sources. Add to this list if you want to implement a new source of email +# notifications. +_notifications_functions = [ + _notifications_from_dashboard_activity_list, +] + + +def get_notifications( + user_dict: dict[str, Any], since: datetime.datetime +) -> list[dict[str, Any]]: + """Return any email notifications for the given user since `since`. + + For example email notifications about activity streams will be returned for + any activities the occurred since `since`. + + :param user_dict: a dictionary representing the user, should contain 'id' + and 'name' + :type user_dict: dictionary + + :param since: datetime after which to return notifications from + :rtype since: datetime.datetime + + :returns: a list of email notifications + :rtype: list of dicts with keys 'subject' and 'body' + + """ + notifications = [] + for function in _notifications_functions: + notifications.extend(function(user_dict, since)) + return notifications + + +def send_notification( + user: dict[str, Any], email_dict: dict[str, Any] +) -> None: + """Email `email_dict` to `user`.""" + import ckan.lib.mailer + + if not user.get("email"): + # FIXME: Raise an exception. + return + + try: + ckan.lib.mailer.mail_recipient( + user["display_name"], + user["email"], + email_dict["subject"], + email_dict["body"], + ) + except ckan.lib.mailer.MailerException: + raise + + +def get_and_send_notifications_for_user(user: dict[str, Any]) -> None: + + # Parse the email_notifications_since config setting, email notifications + # from longer ago than this time will not be sent. + email_notifications_since = config.get( + "ckan.email_notifications_since" + ) + email_notifications_since = string_to_timedelta(email_notifications_since) + email_notifications_since = ( + datetime.datetime.utcnow() - email_notifications_since + ) + + # FIXME: We are accessing model from lib here but I'm not sure what + # else to do unless we add a get_email_last_sent() logic function which + # would only be needed by this lib. + dashboard = model.Dashboard.get(user["id"]) + if dashboard: + email_last_sent = dashboard.email_last_sent + activity_stream_last_viewed = dashboard.activity_stream_last_viewed + since = max( + email_notifications_since, + email_last_sent, + activity_stream_last_viewed, + ) + + notifications = get_notifications(user, since) + # TODO: Handle failures from send_email_notification. + for notification in notifications: + send_notification(user, notification) + + # FIXME: We are accessing model from lib here but I'm not sure what + # else to do unless we add a update_email_last_sent() + # logic function which would only be needed by this lib. + dashboard.email_last_sent = datetime.datetime.utcnow() + model.repo.commit() + + +def get_and_send_notifications_for_all_users() -> None: + context = cast( + Context, + { + "model": model, + "session": model.Session, + "ignore_auth": True, + "keep_email": True, + }, + ) + users = logic.get_action("user_list")(context, {}) + for user in users: + get_and_send_notifications_for_user(user) diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/helpers.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/helpers.py new file mode 100644 index 0000000..a6d8953 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/helpers.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations +from typing import Any, Optional, cast + +import jinja2 +import datetime +from markupsafe import Markup + +import ckan.model as model +import ckan.plugins.toolkit as tk + +from ckan.types import Context +from . import changes + + +def dashboard_activity_stream( + user_id: str, + filter_type: Optional[str] = None, + filter_id: Optional[str] = None, + offset: int = 0, + limit: int = 0, + before: Optional[datetime.datetime] = None, + after: Optional[datetime.datetime] = None, +) -> list[dict[str, Any]]: + """Return the dashboard activity stream of the current user. + + :param user_id: the id of the user + :type user_id: string + + :param filter_type: the type of thing to filter by + :type filter_type: string + + :param filter_id: the id of item to filter by + :type filter_id: string + + :returns: an activity stream as an HTML snippet + :rtype: string + + """ + context = cast(Context, {"user": tk.g.user}) + if filter_type: + action_functions = { + "dataset": "package_activity_list", + "user": "user_activity_list", + "group": "group_activity_list", + "organization": "organization_activity_list", + } + action_function = tk.get_action(action_functions[filter_type]) + return action_function( + context, { + "id": filter_id, + "limit": limit, + "offset": offset, + "before": before, + "after": after + }) + else: + return tk.get_action("dashboard_activity_list")( + context, { + "offset": offset, + "limit": limit, + "before": before, + "after": after + } + ) + + +def recently_changed_packages_activity_stream( + limit: Optional[int] = None, +) -> list[dict[str, Any]]: + if limit: + data_dict = {"limit": limit} + else: + data_dict = {} + context = cast( + Context, {"model": model, "session": model.Session, "user": tk.g.user} + ) + return tk.get_action("recently_changed_packages_activity_list")( + context, data_dict + ) + + +def new_activities() -> Optional[int]: + """Return the number of activities for the current user. + + See :func:`logic.action.get.dashboard_new_activities_count` for more + details. + + """ + if not tk.g.userobj: + return None + action = tk.get_action("dashboard_new_activities_count") + return action({}, {}) + + +def activity_list_select( + pkg_activity_list: list[dict[str, Any]], current_activity_id: str +) -> list[Markup]: + """ + Builds an HTML formatted list of options for the select lists + on the "Changes" summary page. + """ + select_list = [] + template = jinja2.Template( + '', + autoescape=True, + ) + for activity in pkg_activity_list: + entry = tk.h.render_datetime( + activity["timestamp"], with_hours=True, with_seconds=True + ) + select_list.append( + Markup( + template.render( + activity_id=activity["id"], + timestamp=entry, + selected="selected" + if activity["id"] == current_activity_id + else "", + ) + ) + ) + + return select_list + + +def compare_pkg_dicts( + old: dict[str, Any], new: dict[str, Any], old_activity_id: str +) -> list[dict[str, Any]]: + """ + Takes two package dictionaries that represent consecutive versions of + the same dataset and returns a list of detailed & formatted summaries of + the changes between the two versions. old and new are the two package + dictionaries. The function assumes that both dictionaries will have + all of the default package dictionary keys, and also checks for fields + added by extensions and extra fields added by the user in the web + interface. + + Returns a list of dictionaries, each of which corresponds to a change + to the dataset made in this revision. The dictionaries each contain a + string indicating the type of change made as well as other data necessary + to form a detailed summary of the change. + """ + + change_list: list[dict[str, Any]] = [] + + changes.check_metadata_changes(change_list, old, new) + + changes.check_resource_changes(change_list, old, new, old_activity_id) + + # if the dataset was updated but none of the fields we check were changed, + # display a message stating that + if len(change_list) == 0: + change_list.append({"type": "no_change"}) + + return change_list + + +def compare_group_dicts( + old: dict[str, Any], new: dict[str, Any], old_activity_id: str +): + """ + Takes two package dictionaries that represent consecutive versions of + the same organization and returns a list of detailed & formatted summaries + of the changes between the two versions. old and new are the two package + dictionaries. The function assumes that both dictionaries will have + all of the default package dictionary keys, and also checks for fields + added by extensions and extra fields added by the user in the web + interface. + + Returns a list of dictionaries, each of which corresponds to a change + to the dataset made in this revision. The dictionaries each contain a + string indicating the type of change made as well as other data necessary + to form a detailed summary of the change. + """ + change_list: list[dict[str, Any]] = [] + + changes.check_metadata_org_changes(change_list, old, new) + + # if the organization was updated but none of the fields we check + # were changed, display a message stating that + if len(change_list) == 0: + change_list.append({"type": "no_change"}) + + return change_list + + +def activity_show_email_notifications() -> bool: + return tk.config.get("ckan.activity_streams_email_notifications") diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/__init__.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/action.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/action.py new file mode 100644 index 0000000..a42a1c8 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/action.py @@ -0,0 +1,647 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +import logging +import datetime +import json +from typing import Any, Optional + +import ckan.plugins.toolkit as tk + +from ckan.logic import validate +from ckan.types import Context, DataDict, ActionResult +import ckanext.activity.email_notifications as email_notifications + +from . import schema +from ..model import activity as model_activity, activity_dict_save + +log = logging.getLogger(__name__) + + +def send_email_notifications( + context: Context, data_dict: DataDict +) -> ActionResult.SendEmailNotifications: + """Send any pending activity stream notification emails to users. + + You must provide a sysadmin's API key/token in the Authorization header of + the request, or call this action from the command-line via a `ckan notify + send_emails ...` command. + + """ + tk.check_access("send_email_notifications", context, data_dict) + + if not tk.config.get("ckan.activity_streams_email_notifications"): + raise tk.ValidationError( + { + "message": ( + "ckan.activity_streams_email_notifications" + " is not enabled in config" + ) + } + ) + + email_notifications.get_and_send_notifications_for_all_users() + + +def dashboard_mark_activities_old( + context: Context, data_dict: DataDict +) -> ActionResult.DashboardMarkActivitiesOld: + """Mark all the authorized user's new dashboard activities as old. + + This will reset + :py:func:`~ckan.logic.action.get.dashboard_new_activities_count` to 0. + + """ + tk.check_access("dashboard_mark_activities_old", context, data_dict) + model = context["model"] + user_obj = model.User.get(context["user"]) + assert user_obj + user_id = user_obj.id + dashboard = model.Dashboard.get(user_id) + if dashboard: + dashboard.activity_stream_last_viewed = datetime.datetime.utcnow() + if not context.get("defer_commit"): + model.repo.commit() + + +def activity_create( + context: Context, data_dict: DataDict +) -> Optional[dict[str, Any]]: + """Create a new activity stream activity. + + You must be a sysadmin to create new activities. + + :param user_id: the name or id of the user who carried out the activity, + e.g. ``'seanh'`` + :type user_id: string + :param object_id: the name or id of the object of the activity, e.g. + ``'my_dataset'`` + :param activity_type: the type of the activity, this must be an activity + type that CKAN knows how to render, e.g. ``'new package'``, + ``'changed user'``, ``'deleted group'`` etc. + :type activity_type: string + :param data: any additional data about the activity + :type data: dictionary + + :returns: the newly created activity + :rtype: dictionary + + """ + + tk.check_access("activity_create", context, data_dict) + + if not tk.config.get("ckan.activity_streams_enabled"): + return + + model = context["model"] + + # Any revision_id that the caller attempts to pass in the activity_dict is + # ignored and removed here. + if "revision_id" in data_dict: + del data_dict["revision_id"] + + sch = context.get("schema") or schema.default_create_activity_schema() + + data, errors = tk.navl_validate(data_dict, sch, context) + if errors: + raise tk.ValidationError(errors) + + activity = activity_dict_save(data, context) + + if not context.get("defer_commit"): + model.repo.commit() + + log.debug("Created '%s' activity" % activity.activity_type) + return model_activity.activity_dictize(activity, context) + + +@validate(schema.default_activity_list_schema) +@tk.side_effect_free +def user_activity_list( + context: Context, data_dict: DataDict +) -> list[dict[str, Any]]: + """Return a user's public activity stream. + + You must be authorized to view the user's profile. + + + :param id: the id or name of the user + :type id: string + :param offset: where to start getting activity items from + (optional, default: ``0``) + :type offset: int + :param limit: the maximum number of activities to return + (optional, default: ``31`` unless set in site's configuration + ``ckan.activity_list_limit``, upper limit: ``100`` unless set in + site's configuration ``ckan.activity_list_limit_max``) + :type limit: int + :param after: After timestamp + (optional, default: ``None``) + :type after: int, str + :param before: Before timestamp + (optional, default: ``None``) + :type before: int, str + + :rtype: list of dictionaries + + """ + # FIXME: Filter out activities whose subject or object the user is not + # authorized to read. + tk.check_access("user_activity_list", context, data_dict) + + model = context["model"] + + user_ref = data_dict.get("id") # May be user name or id. + user = model.User.get(user_ref) + if user is None: + raise tk.ObjectNotFound() + + offset = data_dict.get("offset", 0) + limit = data_dict["limit"] # defaulted, limited & made an int by schema + after = data_dict.get("after") + before = data_dict.get("before") + + activity_objects = model_activity.user_activity_list( + user.id, + limit=limit, + offset=offset, + after=after, + before=before, + ) + + return model_activity.activity_list_dictize(activity_objects, context) + + +@validate(schema.default_activity_list_schema) +@tk.side_effect_free +def package_activity_list( + context: Context, data_dict: DataDict +) -> list[dict[str, Any]]: + """Return a package's activity stream (not including detail) + + You must be authorized to view the package. + + :param id: the id or name of the package + :type id: string + :param offset: where to start getting activity items from + (optional, default: ``0``) + :type offset: int + :param limit: the maximum number of activities to return + (optional, default: ``31`` unless set in site's configuration + ``ckan.activity_list_limit``, upper limit: ``100`` unless set in + site's configuration ``ckan.activity_list_limit_max``) + :type limit: int + :param after: After timestamp + (optional, default: ``None``) + :type after: int, str + :param before: Before timestamp + (optional, default: ``None``) + :type before: int, str + :param include_hidden_activity: whether to include 'hidden' activity, which + is not shown in the Activity Stream page. Hidden activity includes + activity done by the site_user, such as harvests, which are not shown + in the activity stream because they can be too numerous, or activity by + other users specified in config option `ckan.hide_activity_from_users`. + NB Only sysadmins may set include_hidden_activity to true. + (default: false) + :type include_hidden_activity: bool + :param activity_types: A list of activity types to include in the response + :type activity_types: list + + :param exclude_activity_types: A list of activity types to exclude from the + response + :type exclude_activity_types: list + + :rtype: list of dictionaries + + """ + # FIXME: Filter out activities whose subject or object the user is not + # authorized to read. + include_hidden_activity = data_dict.get("include_hidden_activity", False) + activity_types = data_dict.pop("activity_types", None) + exclude_activity_types = data_dict.pop("exclude_activity_types", None) + + if activity_types is not None and exclude_activity_types is not None: + raise tk.ValidationError( + { + "activity_types": [ + "Cannot be used together with `exclude_activity_types" + ] + } + ) + + tk.check_access("package_activity_list", context, data_dict) + + model = context["model"] + + package_ref = data_dict.get("id") # May be name or ID. + package = model.Package.get(package_ref) + if package is None: + raise tk.ObjectNotFound() + + offset = int(data_dict.get("offset", 0)) + limit = data_dict["limit"] # defaulted, limited & made an int by schema + after = data_dict.get("after") + before = data_dict.get("before") + + activity_objects = model_activity.package_activity_list( + package.id, + limit=limit, + offset=offset, + after=after, + before=before, + include_hidden_activity=include_hidden_activity, + activity_types=activity_types, + exclude_activity_types=exclude_activity_types, + ) + + return model_activity.activity_list_dictize(activity_objects, context) + + +@validate(schema.default_activity_list_schema) +@tk.side_effect_free +def group_activity_list( + context: Context, data_dict: DataDict +) -> list[dict[str, Any]]: + """Return a group's activity stream. + + You must be authorized to view the group. + + :param id: the id or name of the group + :type id: string + :param offset: where to start getting activity items from + (optional, default: ``0``) + :type offset: int + :param limit: the maximum number of activities to return + (optional, default: ``31`` unless set in site's configuration + ``ckan.activity_list_limit``, upper limit: ``100`` unless set in + site's configuration ``ckan.activity_list_limit_max``) + :type limit: int + :param include_hidden_activity: whether to include 'hidden' activity, which + is not shown in the Activity Stream page. Hidden activity includes + activity done by the site_user, such as harvests, which are not shown + in the activity stream because they can be too numerous, or activity by + other users specified in config option `ckan.hide_activity_from_users`. + NB Only sysadmins may set include_hidden_activity to true. + (default: false) + :type include_hidden_activity: bool + + :rtype: list of dictionaries + + """ + # FIXME: Filter out activities whose subject or object the user is not + # authorized to read. + data_dict = dict(data_dict, include_data=False) + include_hidden_activity = data_dict.get("include_hidden_activity", False) + activity_types = data_dict.pop("activity_types", None) + tk.check_access("group_activity_list", context, data_dict) + + group_id = data_dict.get("id") + offset = data_dict.get("offset", 0) + limit = data_dict["limit"] # defaulted, limited & made an int by schema + + # Convert group_id (could be id or name) into id. + group_show = tk.get_action("group_show") + group_id = group_show(context, {"id": group_id})["id"] + + after = data_dict.get("after") + before = data_dict.get("before") + + activity_objects = model_activity.group_activity_list( + group_id, + limit=limit, + offset=offset, + after=after, + before=before, + include_hidden_activity=include_hidden_activity, + activity_types=activity_types + ) + + return model_activity.activity_list_dictize(activity_objects, context) + + +@validate(schema.default_activity_list_schema) +@tk.side_effect_free +def organization_activity_list( + context: Context, data_dict: DataDict +) -> list[dict[str, Any]]: + """Return a organization's activity stream. + + :param id: the id or name of the organization + :type id: string + :param offset: where to start getting activity items from + (optional, default: ``0``) + :type offset: int + :param limit: the maximum number of activities to return + (optional, default: ``31`` unless set in site's configuration + ``ckan.activity_list_limit``, upper limit: ``100`` unless set in + site's configuration ``ckan.activity_list_limit_max``) + :type limit: int + :param include_hidden_activity: whether to include 'hidden' activity, which + is not shown in the Activity Stream page. Hidden activity includes + activity done by the site_user, such as harvests, which are not shown + in the activity stream because they can be too numerous, or activity by + other users specified in config option `ckan.hide_activity_from_users`. + NB Only sysadmins may set include_hidden_activity to true. + (default: false) + :type include_hidden_activity: bool + + :rtype: list of dictionaries + + """ + # FIXME: Filter out activities whose subject or object the user is not + # authorized to read. + include_hidden_activity = data_dict.get("include_hidden_activity", False) + tk.check_access("organization_activity_list", context, data_dict) + + org_id = data_dict.get("id") + offset = data_dict.get("offset", 0) + limit = data_dict["limit"] # defaulted, limited & made an int by schema + activity_types = data_dict.pop("activity_types", None) + + # Convert org_id (could be id or name) into id. + org_show = tk.get_action("organization_show") + org_id = org_show(context, {"id": org_id})["id"] + + after = data_dict.get("after") + before = data_dict.get("before") + + activity_objects = model_activity.organization_activity_list( + org_id, + limit=limit, + offset=offset, + after=after, + before=before, + include_hidden_activity=include_hidden_activity, + activity_types=activity_types + ) + + return model_activity.activity_list_dictize(activity_objects, context) + + +@validate(schema.default_dashboard_activity_list_schema) +@tk.side_effect_free +def recently_changed_packages_activity_list( + context: Context, data_dict: DataDict +) -> list[dict[str, Any]]: + """Return the activity stream of all recently added or changed packages. + + :param offset: where to start getting activity items from + (optional, default: ``0``) + :type offset: int + :param limit: the maximum number of activities to return + (optional, default: ``31`` unless set in site's configuration + ``ckan.activity_list_limit``, upper limit: ``100`` unless set in + site's configuration ``ckan.activity_list_limit_max``) + :type limit: int + + :rtype: list of dictionaries + + """ + # FIXME: Filter out activities whose subject or object the user is not + # authorized to read. + offset = data_dict.get("offset", 0) + limit = data_dict["limit"] # defaulted, limited & made an int by schema + + activity_objects = model_activity.recently_changed_packages_activity_list( + limit=limit, offset=offset + ) + + return model_activity.activity_list_dictize(activity_objects, context) + + +@validate(schema.default_dashboard_activity_list_schema) +@tk.side_effect_free +def dashboard_activity_list( + context: Context, data_dict: DataDict +) -> list[dict[str, Any]]: + """Return the authorized (via login or API key) user's dashboard activity + stream. + + Unlike the activity dictionaries returned by other ``*_activity_list`` + actions, these activity dictionaries have an extra boolean value with key + ``is_new`` that tells you whether the activity happened since the user last + viewed her dashboard (``'is_new': True``) or not (``'is_new': False``). + + The user's own activities are always marked ``'is_new': False``. + + :param offset: where to start getting activity items from + (optional, default: ``0``) + :type offset: int + :param limit: the maximum number of activities to return + (optional, default: ``31`` unless set in site's configuration + ``ckan.activity_list_limit``, upper limit: ``100`` unless set in + site's configuration ``ckan.activity_list_limit_max``) + :type limit: int + + :rtype: list of activity dictionaries + + """ + tk.check_access("dashboard_activity_list", context, data_dict) + + model = context["model"] + user_obj = model.User.get(context["user"]) + assert user_obj + user_id = user_obj.id + offset = data_dict.get("offset", 0) + limit = data_dict["limit"] # defaulted, limited & made an int by schema + before = data_dict.get("before") + after = data_dict.get("after") + # FIXME: Filter out activities whose subject or object the user is not + # authorized to read. + activity_objects = model_activity.dashboard_activity_list( + user_id, limit=limit, offset=offset, before=before, after=after + ) + + activity_dicts = model_activity.activity_list_dictize( + activity_objects, context + ) + + # Mark the new (not yet seen by user) activities. + strptime = datetime.datetime.strptime + fmt = "%Y-%m-%dT%H:%M:%S.%f" + dashboard = model.Dashboard.get(user_id) + last_viewed = None + if dashboard: + last_viewed = dashboard.activity_stream_last_viewed + for activity in activity_dicts: + if activity["user_id"] == user_id: + # Never mark the user's own activities as new. + activity["is_new"] = False + elif last_viewed: + activity["is_new"] = ( + strptime(activity["timestamp"], fmt) > last_viewed + ) + + return activity_dicts + + +@tk.side_effect_free +def dashboard_new_activities_count( + context: Context, data_dict: DataDict +) -> ActionResult.DashboardNewActivitiesCount: + """Return the number of new activities in the user's dashboard. + + Return the number of new activities in the authorized user's dashboard + activity stream. + + Activities from the user herself are not counted by this function even + though they appear in the dashboard (users don't want to be notified about + things they did themselves). + + :rtype: int + + """ + tk.check_access("dashboard_new_activities_count", context, data_dict) + activities = tk.get_action("dashboard_activity_list")(context, data_dict) + return len([activity for activity in activities if activity["is_new"]]) + + +@tk.side_effect_free +def activity_show(context: Context, data_dict: DataDict) -> dict[str, Any]: + """Show details of an item of 'activity' (part of the activity stream). + + :param id: the id of the activity + :type id: string + + :rtype: dictionary + """ + model = context["model"] + activity_id = tk.get_or_bust(data_dict, "id") + + activity = model.Session.query(model_activity.Activity).get(activity_id) + if activity is None: + raise tk.ObjectNotFound() + context["activity"] = activity + + tk.check_access("activity_show", context, data_dict) + + activity = model_activity.activity_dictize(activity, context) + return activity + + +@tk.side_effect_free +def activity_data_show( + context: Context, data_dict: DataDict +) -> dict[str, Any]: + """Show the data from an item of 'activity' (part of the activity + stream). + + For example for a package update this returns just the dataset dict but + none of the activity stream info of who and when the version was created. + + :param id: the id of the activity + :type id: string + :param object_type: 'package', 'user', 'group' or 'organization' + :type object_type: string + + :rtype: dictionary + """ + model = context["model"] + activity_id = tk.get_or_bust(data_dict, "id") + object_type = data_dict.get("object_type") + + activity = model.Session.query(model_activity.Activity).get(activity_id) + if activity is None: + raise tk.ObjectNotFound() + context["activity"] = activity + + tk.check_access("activity_data_show", context, data_dict) + + activity = model_activity.activity_dictize(activity, context) + try: + activity_data = activity["data"] + except KeyError: + raise tk.ObjectNotFound("Could not find data in the activity") + if object_type: + try: + activity_data = activity_data[object_type] + except KeyError: + raise tk.ObjectNotFound( + "Could not find that object_type in the activity" + ) + return activity_data + + +@tk.side_effect_free +def activity_diff(context: Context, data_dict: DataDict) -> dict[str, Any]: + """Returns a diff of the activity, compared to the previous version of the + object + + :param id: the id of the activity + :type id: string + :param object_type: 'package', 'user', 'group' or 'organization' + :type object_type: string + :param diff_type: 'unified', 'context', 'html' + :type diff_type: string + """ + import difflib + + model = context["model"] + activity_id = tk.get_or_bust(data_dict, "id") + object_type = tk.get_or_bust(data_dict, "object_type") + diff_type = data_dict.get("diff_type", "unified") + + tk.check_access("activity_diff", context, data_dict) + + activity = model.Session.query(model_activity.Activity).get(activity_id) + if activity is None: + raise tk.ObjectNotFound() + prev_activity = ( + model.Session.query(model_activity.Activity) + .filter_by(object_id=activity.object_id) + .filter(model_activity.Activity.timestamp < activity.timestamp) + .order_by( + # type_ignore_reason: incomplete SQLAlchemy types + model_activity.Activity.timestamp.desc() # type: ignore + ) + .first() + ) + if prev_activity is None: + raise tk.ObjectNotFound("Previous activity for this object not found") + activity_objs = [prev_activity, activity] + try: + objs = [ + activity_obj.data[object_type] for activity_obj in activity_objs + ] + except KeyError: + raise tk.ObjectNotFound("Could not find object in the activity data") + # convert each object dict to 'pprint'-style + # and split into lines to suit difflib + obj_lines = [ + json.dumps(obj, indent=2, sort_keys=True).split("\n") for obj in objs + ] + + # do the diff + if diff_type == "unified": + # type_ignore_reason: typechecker can't predict number of items + diff_generator = difflib.unified_diff(*obj_lines) # type: ignore + diff = "\n".join(line for line in diff_generator) + elif diff_type == "context": + # type_ignore_reason: typechecker can't predict number of items + diff_generator = difflib.context_diff(*obj_lines) # type: ignore + diff = "\n".join(line for line in diff_generator) + elif diff_type == "html": + # word-wrap lines. Otherwise you get scroll bars for most datasets. + import re + + for obj_index in (0, 1): + wrapped_obj_lines = [] + for line in obj_lines[obj_index]: + wrapped_obj_lines.extend(re.findall(r".{1,70}(?:\s+|$)", line)) + obj_lines[obj_index] = wrapped_obj_lines + # type_ignore_reason: typechecker can't predict number of items + diff = difflib.HtmlDiff().make_table(*obj_lines) # type: ignore + else: + raise tk.ValidationError({"message": "diff_type not recognized"}) + + activities = [ + model_activity.activity_dictize(activity_obj, context) + for activity_obj in activity_objs + ] + + return { + "diff": diff, + "activities": activities, + } diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/auth.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/auth.py new file mode 100644 index 0000000..0df9fd7 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/auth.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +from typing import Optional + +import ckan.authz as authz +import ckan.plugins.toolkit as tk +from ckan.types import Context, DataDict, AuthResult + +from ..model import Activity + + +def _get_activity_object( + context: Context, data_dict: Optional[DataDict] = None +) -> Activity: + try: + return context["activity"] + except KeyError: + if not data_dict: + data_dict = {} + id = data_dict.get("id", None) + if not id: + raise tk.ValidationError( + {"message": "Missing id, can not get Activity object"} + ) + obj = Activity.get(id) + if not obj: + raise tk.ObjectNotFound() + # Save in case we need this again during the request + context["activity"] = obj + return obj + + +def send_email_notifications( + context: Context, data_dict: DataDict +) -> AuthResult: + # Only sysadmins are authorized to send email notifications. + return {"success": False} + + +def activity_create(context: Context, data_dict: DataDict) -> AuthResult: + return {"success": False} + + +@tk.auth_allow_anonymous_access +def dashboard_activity_list( + context: Context, data_dict: DataDict +) -> AuthResult: + # FIXME: context['user'] could be an IP address but that case is not + # handled here. Maybe add an auth helper function like is_logged_in(). + if context.get("user"): + return {"success": True} + else: + return { + "success": False, + "msg": tk._("You must be logged in to access your dashboard."), + } + + +@tk.auth_allow_anonymous_access +def dashboard_new_activities_count( + context: Context, data_dict: DataDict +) -> AuthResult: + # FIXME: This should go through check_access() not call is_authorized() + # directly, but wait until 2939-orgs is merged before fixing this. + # This is so a better not authourized message can be sent. + return authz.is_authorized("dashboard_activity_list", context, data_dict) + + +@tk.auth_allow_anonymous_access +def activity_list(context: Context, data_dict: DataDict) -> AuthResult: + """ + :param id: the id or name of the object (e.g. package id) + :type id: string + :param object_type: The type of the object (e.g. 'package', 'organization', + 'group', 'user') + :type object_type: string + :param include_data: include the data field, containing a full object dict + (otherwise the data field is only returned with the object's title) + :type include_data: boolean + """ + if data_dict["object_type"] not in ( + "package", + "organization", + "group", + "user", + ): + return {"success": False, "msg": "object_type not recognized"} + is_public = authz.check_config_permission("public_activity_stream_detail") + if data_dict.get("include_data") and not is_public: + # The 'data' field of the activity is restricted to users who are + # allowed to edit the object + show_or_update = "update" + else: + # the activity for an object (i.e. the activity metadata) can be viewed + # if the user can see the object + show_or_update = "show" + action_on_which_to_base_auth = "{}_{}".format( + data_dict["object_type"], show_or_update + ) # e.g. 'package_update' + return authz.is_authorized( + action_on_which_to_base_auth, context, {"id": data_dict["id"]} + ) + + +@tk.auth_allow_anonymous_access +def user_activity_list(context: Context, data_dict: DataDict) -> AuthResult: + data_dict["object_type"] = "user" + # TODO: use authz.is_authorized in order to allow chained auth functions. + # TODO: fix the issue in other functions as well + return activity_list(context, data_dict) + + +@tk.auth_allow_anonymous_access +def package_activity_list(context: Context, data_dict: DataDict) -> AuthResult: + data_dict["object_type"] = "package" + return activity_list(context, data_dict) + + +@tk.auth_allow_anonymous_access +def group_activity_list(context: Context, data_dict: DataDict) -> AuthResult: + data_dict["object_type"] = "group" + return activity_list(context, data_dict) + + +@tk.auth_allow_anonymous_access +def organization_activity_list( + context: Context, data_dict: DataDict +) -> AuthResult: + data_dict["object_type"] = "organization" + return activity_list(context, data_dict) + + +@tk.auth_allow_anonymous_access +def activity_show(context: Context, data_dict: DataDict) -> AuthResult: + """ + :param id: the id of the activity + :type id: string + :param include_data: include the data field, containing a full object dict + (otherwise the data field is only returned with the object's title) + :type include_data: boolean + """ + activity = _get_activity_object(context, data_dict) + # NB it would be better to have recorded an activity_type against the + # activity + if "package" in activity.activity_type: + object_type = "package" + else: + return {"success": False, "msg": "object_type not recognized"} + return activity_list( + context, + { + "id": activity.object_id, + "include_data": data_dict["include_data"], + "object_type": object_type, + }, + ) + + +@tk.auth_allow_anonymous_access +def activity_data_show(context: Context, data_dict: DataDict) -> AuthResult: + """ + :param id: the id of the activity + :type id: string + """ + data_dict["include_data"] = True + return activity_show(context, data_dict) + + +@tk.auth_allow_anonymous_access +def activity_diff(context: Context, data_dict: DataDict) -> AuthResult: + """ + :param id: the id of the activity + :type id: string + """ + data_dict["include_data"] = True + return activity_show(context, data_dict) + + +def dashboard_mark_activities_old( + context: Context, data_dict: DataDict +) -> AuthResult: + return authz.is_authorized("dashboard_activity_list", context, data_dict) diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/schema.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/schema.py new file mode 100644 index 0000000..e01f52f --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/schema.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +from typing import cast + +from ckan.logic.schema import validator_args, default_pagination_schema +from ckan.types import Schema, Validator, ValidatorFactory + + +@validator_args +def default_create_activity_schema( + ignore: Validator, + not_missing: Validator, + not_empty: Validator, + unicode_safe: Validator, + convert_user_name_or_id_to_id: Validator, + object_id_validator: Validator, + activity_type_exists: Validator, + ignore_empty: Validator, + ignore_missing: Validator, +): + return cast( + Schema, + { + "id": [ignore], + "timestamp": [ignore], + "user_id": [ + not_missing, + not_empty, + unicode_safe, + convert_user_name_or_id_to_id, + ], + "object_id": [ + not_missing, + not_empty, + unicode_safe, + object_id_validator, + ], + "activity_type": [ + not_missing, + not_empty, + unicode_safe, + activity_type_exists, + ], + "data": [ignore_empty, ignore_missing], + }, + ) + + +@validator_args +def default_dashboard_activity_list_schema( + configured_default: ValidatorFactory, + natural_number_validator: Validator, + limit_to_configured_maximum: ValidatorFactory, + ignore_missing: Validator, + datetime_from_timestamp_validator: Validator, + +): + schema = default_pagination_schema() + schema["limit"] = [ + configured_default("ckan.activity_list_limit", 31), + natural_number_validator, + limit_to_configured_maximum("ckan.activity_list_limit_max", 100), + ] + schema["before"] = [ignore_missing, datetime_from_timestamp_validator] + schema["after"] = [ignore_missing, datetime_from_timestamp_validator] + return schema + + +@validator_args +def default_activity_list_schema( + not_missing: Validator, + unicode_safe: Validator, + configured_default: ValidatorFactory, + natural_number_validator: Validator, + limit_to_configured_maximum: ValidatorFactory, + ignore_missing: Validator, + boolean_validator: Validator, + ignore_not_sysadmin: Validator, + list_of_strings: Validator, + datetime_from_timestamp_validator: Validator, +): + + schema = default_pagination_schema() + schema["id"] = [not_missing, unicode_safe] + schema["limit"] = [ + configured_default("ckan.activity_list_limit", 31), + natural_number_validator, + limit_to_configured_maximum("ckan.activity_list_limit_max", 100), + ] + schema["include_hidden_activity"] = [ + ignore_missing, + ignore_not_sysadmin, + boolean_validator, + ] + schema["activity_types"] = [ignore_missing, list_of_strings] + schema["exclude_activity_types"] = [ignore_missing, list_of_strings] + schema["before"] = [ignore_missing, datetime_from_timestamp_validator] + schema["after"] = [ignore_missing, datetime_from_timestamp_validator] + + return schema diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/validators.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/validators.py new file mode 100644 index 0000000..ac3af06 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/logic/validators.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations +from typing import Any, cast + +import ckan.plugins.toolkit as tk + +from ckan.types import ( + FlattenDataDict, + FlattenKey, + Context, + FlattenErrorDict, + ContextValidator, +) + + +def activity_type_exists(activity_type: Any) -> Any: + """Raises Invalid if there is no registered activity renderer for the + given activity_type. Otherwise returns the given activity_type. + + This just uses object_id_validators as a lookup. + very safe. + + """ + if activity_type in object_id_validators: + return activity_type + else: + raise tk.Invalid("%s: %s" % (tk._("Not found"), tk._("Activity type"))) + + +VALIDATORS_PACKAGE_ACTIVITY_TYPES = { + "new package": "package_id_exists", + "changed package": "package_id_exists", + "deleted package": "package_id_exists", + "follow dataset": "package_id_exists", +} + +VALIDATORS_USER_ACTIVITY_TYPES = { + "new user": "user_id_exists", + "changed user": "user_id_exists", + "follow user": "user_id_exists", +} + +VALIDATORS_GROUP_ACTIVITY_TYPES = { + "new group": "group_id_exists", + "changed group": "group_id_exists", + "deleted group": "group_id_exists", + "follow group": "group_id_exists", +} + +VALIDATORS_ORGANIZATION_ACTIVITY_TYPES = { + "new organization": "group_id_exists", + "changed organization": "group_id_exists", + "deleted organization": "group_id_exists", + "follow organization": "group_id_exists", +} + +# A dictionary mapping activity_type values from activity dicts to functions +# for validating the object_id values from those same activity dicts. +object_id_validators = { + **VALIDATORS_PACKAGE_ACTIVITY_TYPES, + **VALIDATORS_USER_ACTIVITY_TYPES, + **VALIDATORS_GROUP_ACTIVITY_TYPES, + **VALIDATORS_ORGANIZATION_ACTIVITY_TYPES +} + + +def object_id_validator( + key: FlattenKey, + activity_dict: FlattenDataDict, + errors: FlattenErrorDict, + context: Context, +) -> Any: + """Validate the 'object_id' value of an activity_dict. + + Uses the object_id_validators dict (above) to find and call an 'object_id' + validator function for the given activity_dict's 'activity_type' value. + + Raises Invalid if the model given in context contains no object of the + correct type (according to the 'activity_type' value of the activity_dict) + with the given ID. + + Raises Invalid if there is no object_id_validator for the activity_dict's + 'activity_type' value. + + """ + activity_type = activity_dict[("activity_type",)] + if activity_type in object_id_validators: + object_id = activity_dict[("object_id",)] + name = object_id_validators[activity_type] + validator = cast(ContextValidator, tk.get_validator(name)) + return validator(object_id, context) + else: + raise tk.Invalid( + 'There is no object_id validator for activity type "%s"' + % activity_type + ) diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/model/__init__.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/model/__init__.py new file mode 100644 index 0000000..82c3c03 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/model/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations +from typing import Any + +from ckan.types import Context + +from .activity import Activity + + +__all__ = ["Activity"] + + +def activity_dict_save( + activity_dict: dict[str, Any], context: Context +) -> "Activity": + + session = context["session"] + user_id = activity_dict["user_id"] + object_id = activity_dict["object_id"] + activity_type = activity_dict["activity_type"] + if "data" in activity_dict: + data = activity_dict["data"] + else: + data = None + activity_obj = Activity(user_id, object_id, activity_type, data) + session.add(activity_obj) + return activity_obj diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/model/activity.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/model/activity.py new file mode 100644 index 0000000..50ed980 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/model/activity.py @@ -0,0 +1,791 @@ +# encoding: utf-8 +from __future__ import annotations + +import datetime +from typing import Any, Optional, Type, TypeVar, cast +from typing_extensions import TypeAlias + +from sqlalchemy.orm import relationship, backref +from sqlalchemy import ( + types, + Column, + ForeignKey, + or_, + and_, + union_all, + text, +) + +from ckan.common import config +import ckan.model as model +import ckan.model.meta as meta +import ckan.model.domain_object as domain_object +import ckan.model.types as _types +from ckan.model.base import BaseModel +from ckan.lib.dictization import table_dictize + +from ckan.types import Context, Query # noqa + + +__all__ = ["Activity", "ActivityDetail"] + +TActivityDetail = TypeVar("TActivityDetail", bound="ActivityDetail") +QActivity: TypeAlias = "Query[Activity]" + + +class Activity(domain_object.DomainObject, BaseModel): # type: ignore + __tablename__ = "activity" + # the line below handles cases when activity table was already loaded into + # metadata state(via stats extension). Can be removed if stats stop using + # Table object. + __table_args__ = {"extend_existing": True} + + id = Column( + "id", types.UnicodeText, primary_key=True, default=_types.make_uuid + ) + timestamp = Column("timestamp", types.DateTime) + user_id = Column("user_id", types.UnicodeText) + object_id = Column("object_id", types.UnicodeText) + # legacy revision_id values are used by migrate_package_activity.py + revision_id = Column("revision_id", types.UnicodeText) + activity_type = Column("activity_type", types.UnicodeText) + data = Column("data", _types.JsonDictType) + + activity_detail: "ActivityDetail" + + def __init__( + self, + user_id: str, + object_id: str, + activity_type: str, + data: Optional[dict[str, Any]] = None, + ) -> None: + self.id = _types.make_uuid() + self.timestamp = datetime.datetime.utcnow() + self.user_id = user_id + self.object_id = object_id + self.activity_type = activity_type + if data is None: + self.data = {} + else: + self.data = data + + @classmethod + def get(cls, id: str) -> Optional["Activity"]: + """Returns an Activity object referenced by its id.""" + if not id: + return None + + return meta.Session.query(cls).get(id) + + @classmethod + def activity_stream_item( + cls, pkg: model.Package, activity_type: str, user_id: str + ) -> Optional["Activity"]: + import ckan.model + import ckan.logic + + assert activity_type in ("new", "changed"), str(activity_type) + + # Handle 'deleted' objects. + # When the user marks a package as deleted this comes through here as + # a 'changed' package activity. We detect this and change it to a + # 'deleted' activity. + if activity_type == "changed" and pkg.state == "deleted": + if ( + meta.Session.query(cls) + .filter_by(object_id=pkg.id, activity_type="deleted") + .all() + ): + # A 'deleted' activity for this object has already been emitted + # FIXME: What if the object was deleted and then activated + # again? + return None + else: + # Emit a 'deleted' activity for this object. + activity_type = "deleted" + + try: + # We save the entire rendered package dict so we can support + # viewing the past packages from the activity feed. + dictized_package = ckan.logic.get_action("package_show")( + cast( + Context, + { + "model": ckan.model, + "session": ckan.model.Session, + # avoid ckanext-multilingual translating it + "for_view": False, + "ignore_auth": True, + }, + ), + {"id": pkg.id, "include_tracking": False}, + ) + except ckan.logic.NotFound: + # This happens if this package is being purged and therefore has no + # current revision. + # TODO: Purge all related activity stream items when a model object + # is purged. + return None + + actor = meta.Session.query(ckan.model.User).get(user_id) + + return cls( + user_id, + pkg.id, + "%s package" % activity_type, + { + "package": dictized_package, + # We keep the acting user name around so that actions can be + # properly displayed even if the user is deleted in the future. + "actor": actor.name if actor else None, + }, + ) + + +def activity_dictize(activity: Activity, context: Context) -> dict[str, Any]: + return table_dictize(activity, context) + + +def activity_list_dictize( + activity_list: list[Activity], context: Context +) -> list[dict[str, Any]]: + return [activity_dictize(activity, context) for activity in activity_list] + + +# deprecated +class ActivityDetail(domain_object.DomainObject): + __tablename__ = "activity_detail" + id = Column( + "id", types.UnicodeText, primary_key=True, default=_types.make_uuid + ) + activity_id = Column( + "activity_id", types.UnicodeText, ForeignKey("activity.id") + ) + object_id = Column("object_id", types.UnicodeText) + object_type = Column("object_type", types.UnicodeText) + activity_type = Column("activity_type", types.UnicodeText) + data = Column("data", _types.JsonDictType) + + activity = relationship( + Activity, + backref=backref("activity_detail", cascade="all, delete-orphan"), + ) + + def __init__( + self, + activity_id: str, + object_id: str, + object_type: str, + activity_type: str, + data: Optional[dict[str, Any]] = None, + ) -> None: + self.activity_id = activity_id + self.object_id = object_id + self.object_type = object_type + self.activity_type = activity_type + if data is None: + self.data = {} + else: + self.data = data + + @classmethod + def by_activity_id( + cls: Type[TActivityDetail], activity_id: str + ) -> list["TActivityDetail"]: + return ( + model.Session.query(cls).filter_by(activity_id=activity_id).all() + ) + + +def _activities_limit( + q: QActivity, + limit: int, + offset: Optional[int] = None, + revese_order: Optional[bool] = False, +) -> QActivity: + """ + Return an SQLAlchemy query for all activities at an offset with a limit. + + revese_order: + if we want the last activities before a date, we must reverse the + order before limiting. + """ + if revese_order: + q = q.order_by(Activity.timestamp) + else: + # type_ignore_reason: incomplete SQLAlchemy types + q = q.order_by(Activity.timestamp.desc()) # type: ignore + + if offset: + q = q.offset(offset) + if limit: + q = q.limit(limit) + return q + + +def _activities_union_all(*qlist: QActivity) -> QActivity: + """ + Return union of two or more activity queries sorted by timestamp, + and remove duplicates + """ + q: QActivity = ( + model.Session.query(Activity) + .select_entity_from(union_all(*[q.subquery().select() for q in qlist])) + .distinct(Activity.timestamp) + ) + return q + + +def _activities_from_user_query(user_id: str) -> QActivity: + """Return an SQLAlchemy query for all activities from user_id.""" + q = model.Session.query(Activity) + q = q.filter(Activity.user_id == user_id) + return q + + +def _activities_about_user_query(user_id: str) -> QActivity: + """Return an SQLAlchemy query for all activities about user_id.""" + q = model.Session.query(Activity) + q = q.filter(Activity.object_id == user_id) + return q + + +def _user_activity_query(user_id: str, limit: int) -> QActivity: + """Return an SQLAlchemy query for all activities from or about user_id.""" + q1 = _activities_limit(_activities_from_user_query(user_id), limit) + q2 = _activities_limit(_activities_about_user_query(user_id), limit) + return _activities_union_all(q1, q2) + + +def user_activity_list( + user_id: str, + limit: int, + offset: int, + after: Optional[datetime.datetime] = None, + before: Optional[datetime.datetime] = None, +) -> list[Activity]: + """Return user_id's public activity stream. + + Return a list of all activities from or about the given user, i.e. where + the given user is the subject or object of the activity, e.g.: + + "{USER} created the dataset {DATASET}" + "{OTHER_USER} started following {USER}" + etc. + + """ + q1 = _activities_from_user_query(user_id) + q2 = _activities_about_user_query(user_id) + + q = _activities_union_all(q1, q2) + + q = _filter_activitites_from_users(q) + + if after: + q = q.filter(Activity.timestamp > after) + if before: + q = q.filter(Activity.timestamp < before) + + # revert sort queries for "only before" queries + revese_order = after and not before + if revese_order: + q = q.order_by(Activity.timestamp) + else: + # type_ignore_reason: incomplete SQLAlchemy types + q = q.order_by(Activity.timestamp.desc()) # type: ignore + + if offset: + q = q.offset(offset) + if limit: + q = q.limit(limit) + + results = q.all() + + # revert result if required + if revese_order: + results.reverse() + + return results + + +def _package_activity_query(package_id: str) -> QActivity: + """Return an SQLAlchemy query for all activities about package_id.""" + q = model.Session.query(Activity).filter_by(object_id=package_id) + return q + + +def package_activity_list( + package_id: str, + limit: int, + offset: Optional[int] = None, + after: Optional[datetime.datetime] = None, + before: Optional[datetime.datetime] = None, + include_hidden_activity: bool = False, + activity_types: Optional[list[str]] = None, + exclude_activity_types: Optional[list[str]] = None, +) -> list[Activity]: + """Return the given dataset (package)'s public activity stream. + + Returns all activities about the given dataset, i.e. where the given + dataset is the object of the activity, e.g.: + + "{USER} created the dataset {DATASET}" + "{USER} updated the dataset {DATASET}" + etc. + + """ + q = _package_activity_query(package_id) + + if not include_hidden_activity: + q = _filter_activitites_from_users(q) + + if activity_types: + q = _filter_activitites_from_type( + q, include=True, types=activity_types + ) + elif exclude_activity_types: + q = _filter_activitites_from_type( + q, include=False, types=exclude_activity_types + ) + + if after: + q = q.filter(Activity.timestamp > after) + if before: + q = q.filter(Activity.timestamp < before) + + # revert sort queries for "only before" queries + revese_order = after and not before + if revese_order: + q = q.order_by(Activity.timestamp) + else: + # type_ignore_reason: incomplete SQLAlchemy types + q = q.order_by(Activity.timestamp.desc()) # type: ignore + + if offset: + q = q.offset(offset) + if limit: + q = q.limit(limit) + + results = q.all() + + # revert result if required + if revese_order: + results.reverse() + + return results + + +def _group_activity_query(group_id: str) -> QActivity: + """Return an SQLAlchemy query for all activities about group_id. + + Returns a query for all activities whose object is either the group itself + or one of the group's datasets. + + """ + group = model.Group.get(group_id) + if not group: + # Return a query with no results. + return model.Session.query(Activity).filter(text("0=1")) + + q: QActivity = ( + model.Session.query(Activity) + .outerjoin(model.Member, Activity.object_id == model.Member.table_id) + .outerjoin( + model.Package, + and_( + model.Package.id == model.Member.table_id, + model.Package.private == False, # noqa + ), + ) + .filter( + # We only care about activity either on the group itself or on + # packages within that group. FIXME: This means that activity that + # occured while a package belonged to a group but was then removed + # will not show up. This may not be desired but is consistent with + # legacy behaviour. + or_( + # active dataset in the group + and_( + model.Member.group_id == group_id, + model.Member.state == "active", + model.Package.state == "active", + ), + # deleted dataset in the group + and_( + model.Member.group_id == group_id, + model.Member.state == "deleted", + model.Package.state == "deleted", + ), + # (we want to avoid showing changes to an active dataset that + # was once in this group) + # activity the the group itself + Activity.object_id == group_id, + ) + ) + ) + + return q + + +def _organization_activity_query(org_id: str) -> QActivity: + """Return an SQLAlchemy query for all activities about org_id. + + Returns a query for all activities whose object is either the org itself + or one of the org's datasets. + + """ + org = model.Group.get(org_id) + if not org or not org.is_organization: + # Return a query with no results. + return model.Session.query(Activity).filter(text("0=1")) + + q: QActivity = ( + model.Session.query(Activity) + .outerjoin( + model.Package, + and_( + model.Package.id == Activity.object_id, + model.Package.private == False, # noqa + ), + ) + .filter( + # We only care about activity either on the the org itself or on + # packages within that org. + # FIXME: This means that activity that occured while a package + # belonged to a org but was then removed will not show up. This may + # not be desired but is consistent with legacy behaviour. + or_( + model.Package.owner_org == org_id, Activity.object_id == org_id + ) + ) + ) + + return q + + +def group_activity_list( + group_id: str, + limit: int, + offset: int, + after: Optional[datetime.datetime] = None, + before: Optional[datetime.datetime] = None, + include_hidden_activity: bool = False, + activity_types: Optional[list[str]] = None +) -> list[Activity]: + + """Return the given group's public activity stream. + + Returns activities where the given group or one of its datasets is the + object of the activity, e.g.: + + "{USER} updated the group {GROUP}" + "{USER} updated the dataset {DATASET}" + etc. + + """ + q = _group_activity_query(group_id) + + if not include_hidden_activity: + q = _filter_activitites_from_users(q) + + if activity_types: + q = _filter_activitites_from_type( + q, include=True, types=activity_types + ) + + if after: + q = q.filter(Activity.timestamp > after) + if before: + q = q.filter(Activity.timestamp < before) + + # revert sort queries for "only before" queries + revese_order = after and not before + if revese_order: + q = q.order_by(Activity.timestamp) + else: + # type_ignore_reason: incomplete SQLAlchemy types + q = q.order_by(Activity.timestamp.desc()) # type: ignore + + if offset: + q = q.offset(offset) + if limit: + q = q.limit(limit) + + results = q.all() + + # revert result if required + if revese_order: + results.reverse() + + return results + + +def organization_activity_list( + group_id: str, + limit: int, + offset: int, + after: Optional[datetime.datetime] = None, + before: Optional[datetime.datetime] = None, + include_hidden_activity: bool = False, + activity_types: Optional[list[str]] = None +) -> list[Activity]: + """Return the given org's public activity stream. + + Returns activities where the given org or one of its datasets is the + object of the activity, e.g.: + + "{USER} updated the organization {ORG}" + "{USER} updated the dataset {DATASET}" + etc. + + """ + q = _organization_activity_query(group_id) + + if not include_hidden_activity: + q = _filter_activitites_from_users(q) + + if activity_types: + q = _filter_activitites_from_type( + q, include=True, types=activity_types + ) + + if after: + q = q.filter(Activity.timestamp > after) + if before: + q = q.filter(Activity.timestamp < before) + + # revert sort queries for "only before" queries + revese_order = after and not before + if revese_order: + q = q.order_by(Activity.timestamp) + else: + # type_ignore_reason: incomplete SQLAlchemy types + q = q.order_by(Activity.timestamp.desc()) # type: ignore + + if offset: + q = q.offset(offset) + if limit: + q = q.limit(limit) + + results = q.all() + + # revert result if required + if revese_order: + results.reverse() + + return results + + +def _activities_from_users_followed_by_user_query( + user_id: str, limit: int +) -> QActivity: + """Return a query for all activities from users that user_id follows.""" + + # Get a list of the users that the given user is following. + follower_objects = model.UserFollowingUser.followee_list(user_id) + if not follower_objects: + # Return a query with no results. + return model.Session.query(Activity).filter(text("0=1")) + + return _activities_union_all( + *[ + _user_activity_query(follower.object_id, limit) + for follower in follower_objects + ] + ) + + +def _activities_from_datasets_followed_by_user_query( + user_id: str, limit: int +) -> QActivity: + """Return a query for all activities from datasets that user_id follows.""" + # Get a list of the datasets that the user is following. + follower_objects = model.UserFollowingDataset.followee_list(user_id) + if not follower_objects: + # Return a query with no results. + return model.Session.query(Activity).filter(text("0=1")) + + return _activities_union_all( + *[ + _activities_limit( + _package_activity_query(follower.object_id), limit + ) + for follower in follower_objects + ] + ) + + +def _activities_from_groups_followed_by_user_query( + user_id: str, limit: int +) -> QActivity: + """Return a query for all activities about groups the given user follows. + + Return a query for all activities about the groups the given user follows, + or about any of the group's datasets. This is the union of + _group_activity_query(group_id) for each of the groups the user follows. + + """ + # Get a list of the group's that the user is following. + follower_objects = model.UserFollowingGroup.followee_list(user_id) + if not follower_objects: + # Return a query with no results. + return model.Session.query(Activity).filter(text("0=1")) + + return _activities_union_all( + *[ + _activities_limit(_group_activity_query(follower.object_id), limit) + for follower in follower_objects + ] + ) + + +def _activities_from_everything_followed_by_user_query( + user_id: str, limit: int = 0 +) -> QActivity: + """Return a query for all activities from everything user_id follows.""" + q1 = _activities_from_users_followed_by_user_query(user_id, limit) + q2 = _activities_from_datasets_followed_by_user_query(user_id, limit) + q3 = _activities_from_groups_followed_by_user_query(user_id, limit) + return _activities_union_all(q1, q2, q3) + + +def activities_from_everything_followed_by_user( + user_id: str, limit: int, offset: int +) -> list[Activity]: + """Return activities from everything that the given user is following. + + Returns all activities where the object of the activity is anything + (user, dataset, group...) that the given user is following. + + """ + q = _activities_from_everything_followed_by_user_query( + user_id, limit + offset + ) + return _activities_limit(q, limit, offset).all() + + +def _dashboard_activity_query(user_id: str, limit: int = 0) -> QActivity: + """Return an SQLAlchemy query for user_id's dashboard activity stream.""" + q1 = _user_activity_query(user_id, limit) + q2 = _activities_from_everything_followed_by_user_query(user_id, limit) + return _activities_union_all(q1, q2) + + +def dashboard_activity_list( + user_id: str, + limit: int, + offset: int, + before: Optional[datetime.datetime] = None, + after: Optional[datetime.datetime] = None, +) -> list[Activity]: + """Return the given user's dashboard activity stream. + + Returns activities from the user's public activity stream, plus + activities from everything that the user is following. + + This is the union of user_activity_list(user_id) and + activities_from_everything_followed_by_user(user_id). + + """ + q = _dashboard_activity_query(user_id) + + q = _filter_activitites_from_users(q) + + if after: + q = q.filter(Activity.timestamp > after) + if before: + q = q.filter(Activity.timestamp < before) + + # revert sort queries for "only before" queries + revese_order = after and not before + if revese_order: + q = q.order_by(Activity.timestamp) + else: + # type_ignore_reason: incomplete SQLAlchemy types + q = q.order_by(Activity.timestamp.desc()) # type: ignore + + if offset: + q = q.offset(offset) + if limit: + q = q.limit(limit) + + results = q.all() + + # revert result if required + if revese_order: + results.reverse() + + return results + + +def _changed_packages_activity_query() -> QActivity: + """Return an SQLAlchemy query for all changed package activities. + + Return a query for all activities with activity_type '*package', e.g. + 'new_package', 'changed_package', 'deleted_package'. + + """ + q = model.Session.query(Activity) + q = q.filter(Activity.activity_type.endswith("package")) + return q + + +def recently_changed_packages_activity_list( + limit: int, offset: int +) -> list[Activity]: + """Return the site-wide stream of recently changed package activities. + + This activity stream includes recent 'new package', 'changed package' and + 'deleted package' activities for the whole site. + + """ + q = _changed_packages_activity_query() + + q = _filter_activitites_from_users(q) + + return _activities_limit(q, limit, offset).all() + + +def _filter_activitites_from_users(q: QActivity) -> QActivity: + """ + Adds a filter to an existing query object to avoid activities from users + defined in :ref:`ckan.hide_activity_from_users` (defaults to the site user) + """ + users_to_avoid = _activity_stream_get_filtered_users() + if users_to_avoid: + # type_ignore_reason: incomplete SQLAlchemy types + q = q.filter(Activity.user_id.notin_(users_to_avoid)) # type: ignore + + return q + + +def _filter_activitites_from_type( + q: QActivity, types: list[str], include: bool = True +): + """Adds a filter to an existing query object to include or exclude + (include=False) activities based on a list of types. + + """ + if include: + q = q.filter(Activity.activity_type.in_(types)) # type: ignore + else: + q = q.filter(Activity.activity_type.notin_(types)) # type: ignore + return q + + +def _activity_stream_get_filtered_users() -> list[str]: + """ + Get the list of users from the :ref:`ckan.hide_activity_from_users` config + option and return a list of their ids. If the config is not specified, + returns the id of the site user. + """ + users_list = config.get("ckan.hide_activity_from_users") + if not users_list: + from ckan.logic import get_action + + context: Context = {"ignore_auth": True} + site_user = get_action("get_site_user")(context, {}) + users_list = [site_user.get("name")] + + return model.User.user_ids_for_name_or_id(users_list) diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/plugin.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/plugin.py new file mode 100644 index 0000000..fe610c0 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/plugin.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations + +from ckan.common import CKANConfig + +import ckan.plugins as p +import ckan.plugins.toolkit as tk + +from . import subscriptions + + +@tk.blanket.auth_functions +@tk.blanket.actions +@tk.blanket.helpers +@tk.blanket.blueprints +@tk.blanket.validators +class ActivityPlugin(p.SingletonPlugin): + p.implements(p.IConfigurer) + p.implements(p.ISignal) + + # IConfigurer + def update_config(self, config: CKANConfig): + tk.add_template_directory(config, "templates") + tk.add_public_directory(config, "public") + tk.add_resource("assets", "ckanext-activity") + + # ISignal + def get_signal_subscriptions(self): + return subscriptions.get_subscriptions() diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/public/dotted.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/public/dotted.png new file mode 100644 index 0000000000000000000000000000000000000000..fa0ae8040cbdf64a1a9c3214b062aa5c31a36c23 GIT binary patch literal 74 zcmeAS@N?(olHy`uVBq!ia0vp^OhC-S0V1_~EAxPqpr?ytNCji^4sJGsSN0MPnhd(h VVu9wjyxM`144$rjF6*2UngGv~4+;PP literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/subscriptions.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/subscriptions.py new file mode 100644 index 0000000..acdcc7a --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/subscriptions.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- + +from __future__ import annotations +import logging +from typing import Any + +import ckan.plugins.toolkit as tk +import ckan.lib.dictization as dictization + +from ckan import types +from .model import Activity + +log = logging.getLogger(__name__) + + +def get_subscriptions() -> types.SignalMapping: + return { + tk.signals.action_succeeded: [ + {"sender": "bulk_update_public", "receiver": bulk_changed}, + {"sender": "bulk_update_private", "receiver": bulk_changed}, + {"sender": "bulk_update_delete", "receiver": bulk_changed}, + {"sender": "package_create", "receiver": package_changed}, + {"sender": "package_update", "receiver": package_changed}, + {"sender": "package_delete", "receiver": package_changed}, + {"sender": "group_create", "receiver": group_or_org_changed}, + {"sender": "group_update", "receiver": group_or_org_changed}, + {"sender": "group_delete", "receiver": group_or_org_changed}, + { + "sender": "organization_create", + "receiver": group_or_org_changed, + }, + { + "sender": "organization_update", + "receiver": group_or_org_changed, + }, + { + "sender": "organization_delete", + "receiver": group_or_org_changed, + }, + {"sender": "user_create", "receiver": user_changed}, + {"sender": "user_update", "receiver": user_changed}, + ] + } + + +# action, context, data_dict, result +def bulk_changed(sender: str, **kwargs: Any): + for key in ("context", "data_dict"): + if key not in kwargs: + log.warning("Activity subscription ignored") + return + + context: types.Context = kwargs["context"] + datasets = kwargs["data_dict"].get("datasets") + model = context["model"] + + user = context["user"] + user_obj = model.User.get(user) + if user_obj: + user_id = user_obj.id + else: + user_id = "not logged in" + for dataset in datasets: + entity = model.Package.get(dataset) + assert entity + + activity = Activity.activity_stream_item(entity, "changed", user_id) + model.Session.add(activity) + + if not context.get("defer_commit"): + model.Session.commit() + + +# action, context, data_dict, result +def package_changed(sender: str, **kwargs: Any): + for key in ("result", "context", "data_dict"): + if key not in kwargs: + log.warning("Activity subscription ignored") + return + + type_ = "new" if sender == "package_create" else "changed" + + context: types.Context = kwargs["context"] + result: types.ActionResult.PackageUpdate = kwargs["result"] + data_dict = kwargs["data_dict"] + + if not result: + id_ = data_dict["id"] + elif isinstance(result, str): + id_ = result + else: + id_ = result["id"] + + pkg = context["model"].Package.get(id_) + assert pkg + + if pkg.private: + return + + user_obj = context["model"].User.get(context["user"]) + if user_obj: + user_id = user_obj.id + else: + user_id = "not logged in" + + activity = Activity.activity_stream_item(pkg, type_, user_id) + context["session"].add(activity) + if not context.get("defer_commit"): + context["session"].commit() + + +# action, context, data_dict, result +def group_or_org_changed(sender: str, **kwargs: Any): + for key in ("result", "context", "data_dict"): + if key not in kwargs: + log.warning("Activity subscription ignored") + return + + context: types.Context = kwargs["context"] + result: types.ActionResult.GroupUpdate = kwargs["result"] + data_dict = kwargs["data_dict"] + + group = context["model"].Group.get( + result["id"] if result else data_dict["id"] + ) + assert group + + type_, action = sender.split("_") + + user_obj = context["model"].User.get(context["user"]) + assert user_obj + + activity_dict: dict[str, Any] = { + "user_id": user_obj.id, + "object_id": group.id, + } + + if group.state == "deleted" or action == "delete": + activity_type = f"deleted {type_}" + elif action == "create": + activity_type = f"new {type_}" + else: + activity_type = f"changed {type_}" + + activity_dict["activity_type"] = activity_type + + activity_dict["data"] = { + "group": dictization.table_dictize(group, context) + } + activity_create_context = tk.fresh_context(context) + activity_create_context['ignore_auth'] = True + tk.get_action("activity_create")(activity_create_context, activity_dict) + + +# action, context, data_dict, result +def user_changed(sender: str, **kwargs: Any): + for key in ("result", "context", "data_dict"): + if key not in kwargs: + log.warning("Activity subscription ignored") + return + + context: types.Context = kwargs["context"] + result: types.ActionResult.UserUpdate = kwargs["result"] + + if sender == "user_create": + activity_type = "new user" + else: + activity_type = "changed user" + + activity_dict = { + "user_id": result["id"], + "object_id": result["id"], + "activity_type": activity_type, + } + activity_create_context = tk.fresh_context(context) + activity_create_context['ignore_auth'] = True + tk.get_action("activity_create")(activity_create_context, activity_dict) diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/activity_streams/activity_stream_email_notifications.text b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/activity_streams/activity_stream_email_notifications.text new file mode 100644 index 0000000..5cafaae --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/activity_streams/activity_stream_email_notifications.text @@ -0,0 +1,7 @@ +{% set num = activities|length %}{{ ngettext("You have {num} new activity on your {site_title} dashboard", "You have {num} new activities on your {site_title} dashboard", num).format(site_title=g.site_title if g else site_title, num=num) }} {{ _('To view your dashboard, click on this link:') }} + +{% url_for 'activity.dashboard', _external=True %} + +{{ _('You can turn off these email notifications in your {site_title} preferences. To change your preferences, click on this link:').format(site_title=g.site_title if g else site_title) }} + +{% url_for 'user.edit', _external=True %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/ajax_snippets/dashboard.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/ajax_snippets/dashboard.html new file mode 100644 index 0000000..c245c23 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/ajax_snippets/dashboard.html @@ -0,0 +1,4 @@ +{# Snippet for unit testing dashboard.js #} +
+ {% snippet 'user/snippets/followee_dropdown.html', context={}, followees=[{"dict": {"id": 1}, "display_name": "Test followee" }, {"dict": {"id": 2}, "display_name": "Not valid" }] %} +
diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/base.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/base.html new file mode 100644 index 0000000..be67977 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/base.html @@ -0,0 +1,6 @@ +{% ckan_extends %} + +{% block styles %} + {{ super() }} + {% asset 'ckanext-activity/activity-css' %} +{% endblock %} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/activity_stream.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/activity_stream.html new file mode 100644 index 0000000..0a7484e --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/activity_stream.html @@ -0,0 +1,15 @@ +{% extends "group/read_base.html" %} + +{% block subtitle %}{{ _('Activity Stream') }} {{ g.template_title_delimiter }} {{ super() }}{% endblock %} + +{% block primary_content_inner %} + + {% if activity_types is defined %} + {% snippet 'snippets/activity_type_selector.html', id=id, activity_type=activity_type, activity_types=activity_types, blueprint='activity.group_activity' %} + {% endif %} + + {% snippet 'snippets/stream.html', activity_stream=activity_stream, id=id, object_type='group' %} + + {% snippet 'snippets/pagination.html', newer_activities_url=newer_activities_url, older_activities_url=older_activities_url %} + +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/changes.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/changes.html new file mode 100644 index 0000000..00b7307 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/changes.html @@ -0,0 +1,64 @@ +{% extends "group/read_base.html" %} + +{% block subtitle %}{{ group_dict.name }} {{ g.template_title_delimiter }} Changes {{ g.template_title_delimiter }} {{ super() }}{% endblock %} + +{% block breadcrumb_content_selected %}{% endblock %} + +{% block breadcrumb_content %} + {{ super() }} +
  • {% link_for _('Changes'), named_route='activity.group_activity', id=group_dict.name %}
  • +
  • {% link_for activity_diffs[0].activities[1].id|truncate(30), named_route='activity.group_changes', id=activity_diffs[0].activities[1].id %}
  • +{% endblock %} + +{% block primary %} +
    +
    + {% block group_changes_header %} +

    {{ _('Changes') }}

    + {% endblock %} + + {% set select_list1 = h.activity_list_select(group_activity_list, activity_diffs[-1].activities[0].id) %} + {% set select_list2 = h.activity_list_select(group_activity_list, activity_diffs[0].activities[1].id) %} +
    + + + View changes from + to + +
    + +
    + + {# iterate through the list of activity diffs #} +
    + {% for i in range(activity_diffs|length) %} + {% snippet "group/snippets/item_group.html", activity_diff=activity_diffs[i], group_dict=group_dict %} + + {# TODO: display metadata for more than most recent change #} + {% if i == 0 %} + {# button to show JSON metadata diff for the most recent change - not shown by default #} + + + {% endif %} + +
    + {% endfor %} +
    +
    +{% endblock %} + +{% block secondary %}{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/read_base.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/read_base.html new file mode 100644 index 0000000..03ce862 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/read_base.html @@ -0,0 +1,6 @@ +{% ckan_extends %} + +{% block content_primary_nav %} + {{ super() }} + {{ h.build_nav_icon('activity.group_activity', _('Activity Stream'), id=group_dict.name, offset=0, icon='clock') }} +{% endblock content_primary_nav %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/snippets/item_group.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/snippets/item_group.html new file mode 100644 index 0000000..915fabe --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/group/snippets/item_group.html @@ -0,0 +1,10 @@ +{{ gettext('On %(timestamp)s, %(username)s:', timestamp=h.render_datetime(activity_diff.activities[1].timestamp, with_hours=True, with_seconds=True), username=h.linked_user(activity_diff.activities[1].user_id)) }} + +{% set changes = h.compare_group_dicts(activity_diff.activities[0].data.group, activity_diff.activities[1].data.group, activity_diff.activities[0].id) %} +
      + {% for change in changes %} + {% snippet "snippets/group_changes/{}.html".format( + change.type), change=change %} +
      + {% endfor %} +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/header.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/header.html new file mode 100644 index 0000000..4bd0dff --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/header.html @@ -0,0 +1,15 @@ +{% ckan_extends %} + +{% block header_dashboard %} + {% set new_activities = h.new_activities() %} +
  • + {% set notifications_tooltip = ngettext('Dashboard (%(num)d new item)', 'Dashboard (%(num)d new items)', + new_activities) + %} + + + {{ _('Dashboard') }} + {{ new_activities }} + +
  • +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/activity_stream.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/activity_stream.html new file mode 100644 index 0000000..c69b210 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/activity_stream.html @@ -0,0 +1,14 @@ +{% extends "organization/read_base.html" %} + +{% block subtitle %}{{ _('Activity Stream') }} {{ g.template_title_delimiter }} {{ super() }}{% endblock %} + +{% block primary_content_inner %} + + {% if activity_types is defined %} + {% snippet 'snippets/activity_type_selector.html', id=id, activity_type=activity_type, activity_types=activity_types, blueprint='activity.organization_activity' %} + {% endif %} + + {% snippet 'snippets/stream.html', activity_stream=activity_stream, id=id, object_type='organization', group_type=group_type %} + + {% snippet 'snippets/pagination.html', newer_activities_url=newer_activities_url, older_activities_url=older_activities_url %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/changes.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/changes.html new file mode 100644 index 0000000..7a85b19 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/changes.html @@ -0,0 +1,64 @@ +{% extends "organization/read_base.html" %} + +{% block subtitle %}{{ group_dict.name }} {{ g.template_title_delimiter }} Changes {{ g.template_title_delimiter }} {{ super() }}{% endblock %} + +{% block breadcrumb_content_selected %}{% endblock %} + +{% block breadcrumb_content %} + {{ super() }} +
  • {% link_for _('Changes'), named_route='activity.organization_activity', id=group_dict.name %}
  • +
  • {% link_for activity_diffs[0].activities[1].id|truncate(30), named_route='activity.organization_changes', id=activity_diffs[0].activities[1].id %}
  • +{% endblock %} + +{% block primary %} +
    +
    + {% block organization_changes_header %} +

    {{ _('Changes') }}

    + {% endblock %} + + {% set select_list1 = h.activity_list_select(group_activity_list, activity_diffs[-1].activities[0].id) %} + {% set select_list2 = h.activity_list_select(group_activity_list, activity_diffs[0].activities[1].id) %} +
    + + + View changes from + to + +
    + +
    + + {# iterate through the list of activity diffs #} +
    + {% for i in range(activity_diffs|length) %} + {% snippet "organization/snippets/item_organization.html", activity_diff=activity_diffs[i], group_dict=group_dict %} + + {# TODO: display metadata for more than most recent change #} + {% if i == 0 %} + {# button to show JSON metadata diff for the most recent change - not shown by default #} + + + {% endif %} + +
    + {% endfor %} +
    +
    +{% endblock %} + +{% block secondary %}{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/read_base.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/read_base.html new file mode 100644 index 0000000..2aa814f --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/read_base.html @@ -0,0 +1,6 @@ +{% ckan_extends %} + +{% block content_primary_nav %} + {{ super() }} + {{ h.build_nav_icon('activity.organization_activity', _('Activity Stream'), id=group_dict.name, offset=0, icon='clock') }} +{% endblock content_primary_nav %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/snippets/item_organization.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/snippets/item_organization.html new file mode 100644 index 0000000..5e2ee10 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/organization/snippets/item_organization.html @@ -0,0 +1,10 @@ +{{ gettext('On %(timestamp)s, %(username)s:', timestamp=h.render_datetime(activity_diff.activities[1].timestamp, with_hours=True, with_seconds=True), username=h.linked_user(activity_diff.activities[1].user_id)) }} + +{% set changes = h.compare_group_dicts(activity_diff.activities[0].data.group, activity_diff.activities[1].data.group, activity_diff.activities[0].id) %} +
      + {% for change in changes %} + {% snippet "snippets/organization_changes/{}.html".format( + change.type), change=change %} +
      + {% endfor %} +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/activity_stream.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/activity_stream.html new file mode 100644 index 0000000..40e663e --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/activity_stream.html @@ -0,0 +1,24 @@ +{% extends "package/read_base.html" %} + +{% block subtitle %}{{ _('Activity Stream') }} {{ g.template_title_delimiter }} {{ super() }}{% endblock %} + +{% block primary_content_inner %} + {% if activity_types is defined %} + {% snippet 'snippets/activity_type_selector.html', id=id, activity_type=activity_type, activity_types=activity_types, blueprint='dataset.activity' %} + {% endif %} + + {% if activity_stream|length > 0 %} + {% snippet 'snippets/stream.html', activity_stream=activity_stream, id=id, object_type='package' %} + {% else %} +

    + {% if activity_type %} + {{ _('No activity found for this type') }} + {% else %} + {{ _('No activity found') }}. + {% endif %} +

    + {% endif %} + + {% snippet 'snippets/pagination.html', newer_activities_url=newer_activities_url, older_activities_url=older_activities_url %} + +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/changes.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/changes.html new file mode 100644 index 0000000..51052be --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/changes.html @@ -0,0 +1,64 @@ +{% extends "package/base.html" %} + +{% block subtitle %}{{ pkg_dict.name }} {{ g.template_title_delimiter }} Changes {{ g.template_title_delimiter }} {{ super() }}{% endblock %} + +{% block breadcrumb_content_selected %}{% endblock %} + +{% block breadcrumb_content %} + {{ super() }} +
  • {% link_for _('Changes'), named_route='activity.package_activity', id=pkg_dict.name %}
  • +
  • {% link_for activity_diffs[0].activities[1].id|truncate(30), named_route='activity.package_changes', id=activity_diffs[0].activities[1].id %}
  • +{% endblock %} + +{% block primary %} +
    +
    + {% block package_changes_header %} +

    {{ _('Changes') }}

    + {% endblock %} + + {% set select_list1 = h.activity_list_select(pkg_activity_list, activity_diffs[-1].activities[0].id) %} + {% set select_list2 = h.activity_list_select(pkg_activity_list, activity_diffs[0].activities[1].id) %} +
    + + + View changes from + to + +
    + +
    + + {# iterate through the list of activity diffs #} +
    + {% for i in range(activity_diffs|length) %} + {% snippet "package/snippets/change_item.html", activity_diff=activity_diffs[i], pkg_dict=pkg_dict %} + + {# TODO: display metadata for more than most recent change #} + {% if i == 0 %} + {# button to show JSON metadata diff for the most recent change - not shown by default #} + + + {% endif %} + +
    + {% endfor %} +
    +
    +{% endblock %} + +{% block secondary %}{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/history.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/history.html new file mode 100644 index 0000000..8a29866 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/history.html @@ -0,0 +1,23 @@ +{% extends "package/read.html" %} + +{% block package_description %} + {% block package_archive_notice %} +
    + {% trans url=h.url_for(pkg.type ~ '.read', id=pkg.id) %} + You're currently viewing an old version of this dataset. + Data files may not match the old version of the metadata. + View the current version. + {% endtrans %} +
    + {% endblock %} + + {{ super() }} +{% endblock package_description %} + + +{% block package_resources %} + {% snippet "package/snippets/resources_list.html", pkg=pkg, resources=pkg.resources, is_activity_archive=true %} +{% endblock %} + +{% block content_action %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/read_base.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/read_base.html new file mode 100644 index 0000000..c0ec714 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/read_base.html @@ -0,0 +1,6 @@ +{% ckan_extends %} + +{% block content_primary_nav %} + {{ super() }} + {{ h.build_nav_icon('activity.package_activity', _('Activity Stream'), id=pkg.id if is_activity_archive else pkg.name, icon='clock') }} +{% endblock content_primary_nav %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/resource_history.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/resource_history.html new file mode 100644 index 0000000..aeee719 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/resource_history.html @@ -0,0 +1,26 @@ +{% extends "package/resource_read.html" %} + +{% block action_manage %} +{% endblock action_manage %} + + +{% block resource_content %} + {% block package_archive_notice %} +
    + {% trans url=h.url_for(pkg.type ~ '.read', id=pkg.id) %} + You're currently viewing an old version of this dataset. + Data files may not match the old version of the metadata. + View the current version. + {% endtrans %} +
    + {% endblock %} + {{ super() }} +{% endblock %} + +{% block data_preview %} +{% endblock %} + + +{% block resources_list %} + {% snippet "package/snippets/resources.html", pkg=pkg, active=res.id, action='read', is_activity_archive=true %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/change_item.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/change_item.html new file mode 100644 index 0000000..df7aacf --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/change_item.html @@ -0,0 +1,10 @@ +{{ gettext('On %(timestamp)s, %(username)s:', timestamp=h.render_datetime(activity_diff.activities[1].timestamp, with_hours=True, with_seconds=True), username=h.linked_user(activity_diff.activities[1].user_id)) }} + +{% set changes = h.compare_pkg_dicts(activity_diff.activities[0].data.package, activity_diff.activities[1].data.package, activity_diff.activities[0].id) %} +
      + {% for change in changes %} + {% snippet "snippets/changes/{}.html".format( + change.type), change=change, pkg_dict=pkg_dict %} +
      + {% endfor %} +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/resource_item.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/resource_item.html new file mode 100644 index 0000000..b1fed10 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/resource_item.html @@ -0,0 +1,16 @@ +{% ckan_extends %} + +{% block explore_view %} + {% if is_activity_archive %} +
  • + + + {{ _('More information') }} + +
  • + {% else %} + {{ super() }} + {% endif %} + + +{% endblock explore_view %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/resources.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/resources.html new file mode 100644 index 0000000..08a9835 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/resources.html @@ -0,0 +1,12 @@ +{% ckan_extends %} + + +{% block resources_list %} + +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/resources_list.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/resources_list.html new file mode 100644 index 0000000..40afa88 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/package/snippets/resources_list.html @@ -0,0 +1,15 @@ +{% ckan_extends %} + +{% block resource_list_inner %} + {% for resource in resources %} + {% if is_activity_archive %} + {% set url = h.url_for("activity.resource_history", id=pkg.id, resource_id=resource.id, activity_id=request.view_args.activity_id) %} + {% endif %} + {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, can_edit=false if is_activity_archive else can_edit, is_activity_archive=is_activity_archive, url=url %} + {% endfor %} +{% endblock %} + + +{% block resource_list_empty %} +

    {{ _('This dataset has no data') }}

    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/page.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/page.html new file mode 100644 index 0000000..471757b --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/page.html @@ -0,0 +1,6 @@ +{% ckan_extends %} + +{% block scripts %} + {{ super() }} + {% asset "ckanext-activity/activity" %} +{% endblock scripts %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/added_tag.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/added_tag.html new file mode 100644 index 0000000..ad07377 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/added_tag.html @@ -0,0 +1,15 @@ +
  • + + + + + {{ _('{actor} added the tag {tag} to the dataset {dataset}').format( + actor=ah.actor(activity), + dataset=ah.dataset(activity), + tag=ah.tag(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_group.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_group.html new file mode 100644 index 0000000..13f0cb7 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_group.html @@ -0,0 +1,20 @@ +
  • + + + + + {{ _('{actor} updated the group {group}').format( + actor=ah.actor(activity), + group=ah.group(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + {% if can_show_activity_detail %} +  |  + + {{ _('Changes') }} + + {% endif %} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_organization.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_organization.html new file mode 100644 index 0000000..e63a9bb --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_organization.html @@ -0,0 +1,20 @@ +
  • + + + + + {{ _('{actor} updated the organization {organization}').format( + actor=ah.actor(activity), + organization=ah.organization(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + {% if can_show_activity_detail %} +  |  + + {{ _('Changes') }} + + {% endif %} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_package.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_package.html new file mode 100644 index 0000000..f8eee1e --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_package.html @@ -0,0 +1,26 @@ +{% set dataset_type = activity.data.package.type or 'dataset' %} +
  • + + + + + {{ _('{actor} updated the {dataset_type} {dataset}').format( + actor=ah.actor(activity), + dataset_type=h.humanize_entity_type('package', dataset_type, 'activity_record') or _('dataset'), + dataset=ah.dataset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + {% if can_show_activity_detail %} +  |  + + {{ _('View this version') }} + +  |  + + {{ _('Changes') }} + + {% endif %} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_resource.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_resource.html new file mode 100644 index 0000000..997f76b --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_resource.html @@ -0,0 +1,15 @@ +
  • + + + + + {{ _('{actor} updated the resource {resource} in the dataset {dataset}').format( + actor=ah.actor(activity), + resource=ah.resource(activity), + dataset=ah.datset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_user.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_user.html new file mode 100644 index 0000000..6bc2b0d --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/changed_user.html @@ -0,0 +1,13 @@ +
  • + + + + + {{ _('{actor} updated their profile').format( + actor=ah.actor(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_group.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_group.html new file mode 100644 index 0000000..517ce73 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_group.html @@ -0,0 +1,14 @@ +
  • + + + + + {{ _('{actor} deleted the group {group}').format( + actor=ah.actor(activity), + group=ah.group(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_organization.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_organization.html new file mode 100644 index 0000000..8b09cf2 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_organization.html @@ -0,0 +1,14 @@ +
  • + + + + + {{ _('{actor} deleted the organization {organization}').format( + actor=ah.actor(activity), + organization=ah.organization(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_package.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_package.html new file mode 100644 index 0000000..0a733e6 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_package.html @@ -0,0 +1,17 @@ +{% set dataset_type = activity.data.package.type or 'dataset' %} + +
  • + + + + + {{ _('{actor} deleted the {dataset_type} {dataset}').format( + actor=ah.actor(activity), + dataset_type=h.humanize_entity_type('package', dataset_type, 'activity_record') or _('dataset'), + dataset=ah.dataset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_resource.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_resource.html new file mode 100644 index 0000000..7334e58 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/deleted_resource.html @@ -0,0 +1,15 @@ +
  • + + + + + {{ _('{actor} deleted the resource {resource} from the dataset {dataset}').format( + actor=ah.actor(activity), + resource=ah.resource(activity), + dataset=ah.dataset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/fallback.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/fallback.html new file mode 100644 index 0000000..abaef61 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/fallback.html @@ -0,0 +1,38 @@ +{# + Fallback template for displaying an activity. + It's not pretty, but it is better than TemplateNotFound. + + Params: + activity - the Activity dict + can_show_activity_detail - whether we should render detail about the activity (i.e. "as it was" and diff, alternatively will just display the metadata about the activity) + id - the id or current name of the object (e.g. package name, user id) + ah - dict of template macros to render linked: actor, dataset, organization, user, group +#} +
  • + + + + + {{ _('{actor} {activity_type}').format( + actor=ah.actor(activity), + activity_type=activity.activity_type + )|safe }} + {% if activity.data.package %} + {{ ah.dataset(activity) }} + {% endif %} + {% if activity.data.package %} + {{ ah.dataset(activity) }} + {% endif %} + {% if activity.data.group %} + {# do our best to differentiate between org & group #} + {% if 'group' in activity.activity_type %} + {{ ah.group(activity) }} + {% else %} + {{ ah.organization(activity) }} + {% endif %} + {% endif %} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/follow_dataset.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/follow_dataset.html new file mode 100644 index 0000000..8bd58f0 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/follow_dataset.html @@ -0,0 +1,14 @@ +
  • + + + + + {{ _('{actor} started following {dataset}').format( + actor=ah.actor(activity), + dataset=ah.dataset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/follow_group.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/follow_group.html new file mode 100644 index 0000000..d26a701 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/follow_group.html @@ -0,0 +1,14 @@ +
  • + + + + + {{ _('{actor} started following {group}').format( + actor=ah.actor(activity), + group=ah.group(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/follow_user.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/follow_user.html new file mode 100644 index 0000000..914be5b --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/follow_user.html @@ -0,0 +1,14 @@ +
  • + + + + + {{ _('{actor} started following {user}').format( + actor=ah.actor(activity), + user=ah.user(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_group.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_group.html new file mode 100644 index 0000000..ffa9ab3 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_group.html @@ -0,0 +1,14 @@ +
  • + + + + + {{ _('{actor} created the group {group}').format( + actor=ah.actor(activity), + group=ah.group(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_organization.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_organization.html new file mode 100644 index 0000000..2dc2a93 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_organization.html @@ -0,0 +1,14 @@ +
  • + + + + + {{ _('{actor} created the organization {organization}').format( + actor=ah.actor(activity), + organization=ah.organization(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_package.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_package.html new file mode 100644 index 0000000..b607ce8 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_package.html @@ -0,0 +1,22 @@ +{% set dataset_type = activity.data.package.type or 'dataset' %} +
  • + + + + + {{ _('{actor} created the {dataset_type} {dataset}').format( + actor=ah.actor(activity), + dataset_type=h.humanize_entity_type('package', dataset_type,'activity_record') or _('dataset'), + dataset=ah.dataset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + {% if can_show_activity_detail %} +  |  + + {{ _('View this version') }} + + {% endif %} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_resource.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_resource.html new file mode 100644 index 0000000..057089e --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_resource.html @@ -0,0 +1,15 @@ +
  • + + + + + {{ _('{actor} added the resource {resource} to the dataset {dataset}').format( + actor=ah.actor(activity), + resource=ah.resource(activity), + dataset=ah.dataset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_user.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_user.html new file mode 100644 index 0000000..25bae1a --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/new_user.html @@ -0,0 +1,13 @@ +
  • + + + + + {{ _('{actor} signed up').format( + actor=ah.actor(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/removed_tag.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/removed_tag.html new file mode 100644 index 0000000..992fcb8 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activities/removed_tag.html @@ -0,0 +1,15 @@ +
  • + + + + + {{ _('{actor} removed the tag {tag} from the dataset {dataset}').format( + actor=ah.actor(activity), + tag=ah.tag(activity), + dataset=ah.dataset(dataset) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activity_type_selector.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activity_type_selector.html new file mode 100644 index 0000000..ba2d5e0 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/activity_type_selector.html @@ -0,0 +1,25 @@ +{# + Renders a select element to filter the activity stream + + It uses the activity-stream.js module to dinamically request for a new URl upon selection. + + id - the id or current name of the object (e.g. package name, user id) + activity_type - the current selected activity type + activity_types - the list of activity types the user can filter on + blueprint - blueprint to call when selecting an activity type to filter by (eg: dataset.activity) +#} + +
    + + +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/author.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/author.html new file mode 100644 index 0000000..41627bd --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/author.html @@ -0,0 +1,30 @@ +
  • +

    + {% if change.method == "change" %} + + {{ _('Set author of {pkg_link} to {new_author} (previously {old_author})').format( + pkg_link=h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + new_author = change.new_author, + old_author = change.old_author + ) }} + + {% elif change.method == "add" %} + + {{ _('Set author of {pkg_link} to {new_author}').format( + pkg_link=h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + new_author = change.new_author + ) }} + + {% elif change.method == "remove" %} + + {{ _('Removed author from {pkg_link}').format( + pkg_link=h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id) + ) }} + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/author_email.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/author_email.html new file mode 100644 index 0000000..edc4403 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/author_email.html @@ -0,0 +1,47 @@ +
  • +

    + {% set pkg_link = (h.url_for('dataset.read', id=change.pkg_id))%} + {% if change.method == "change" %} + + {{ _('Set author email of ')}} + + {{ change.title }} + + + {{ _('to') }} + + {{change.new_author_email}} + + + {{_('( previously ')}} + + {{change.old_author_email}} + + {{ _(')') }} + + {% elif change.method == "add" %} + + {{_('Set author email of')}} + + {{change.title}} + + + {{ _('to') }} + + {{ change.new_author_email }} + + + {% elif change.method == "remove" %} + + {{ _('Removed author email from')}} + + {{change.title}} + + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/delete_resource.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/delete_resource.html new file mode 100644 index 0000000..6bf96b3 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/delete_resource.html @@ -0,0 +1,10 @@ +
  • +

    + {{ _('Deleted resource {resource_link} from {pkg_link}').format( + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', qualified=True, id=change.pkg_id, + resource_id = change.resource_id, activity_id=change.old_activity_id), + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + ) }} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/extension_fields.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/extension_fields.html new file mode 100644 index 0000000..af1429e --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/extension_fields.html @@ -0,0 +1,9 @@ +
  • +

    + {{ _('Changed value of field {key} to {value} in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + key = change.key, + value = change.value + )}} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/extra_fields.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/extra_fields.html new file mode 100644 index 0000000..e8bdd73 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/extra_fields.html @@ -0,0 +1,85 @@ +
  • +

    + {% if change.method == "add_one_value" %} + + {{ _('Added field {key} with value {value} to {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + key = change.key, + value = change.value + )}} + + {% elif change.method == "add_one_no_value" %} + + {{ _('Added field {key} to {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + key = change.key + )}} + + {% elif change.method == "add_multiple" %} + + {{ _('Added the following fields to {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id) + )}} +

      + {% for item in change.key_list %} +
    • + {% if change.value_list[item] != "" %} + {{ _('{key} with value {value}').format( + key = item, + value = change.value_list[item] + )|safe }} + {% else %} + {{ _('{key}').format( + key = item + )|safe }} + {% endif %} +
    • + {% endfor %} +
    + + {% elif change.method == "change_with_old_value" %} + + {{ _('Changed value of field {key} to {new_val} (previously {old_val}) in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + key = change.key, + new_val = change.new_value, + old_val = change.old_value + )}} + + {% elif change.method == "change_no_old_value" %} + + {{ _('Changed value of field {key} to {new_val} in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + key = change.key, + new_val = change.new_value, + ) }} + + {% elif change.method == "remove_one" %} + + {{ _('Removed field {key} from {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + key = change.key, + ) }} + + {% elif change.method == "remove_multiple" %} + + {{ _('Removed the following fields from {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id) + ) }} +
      + {% for item in change.key_list %} +
    • + {{ _('{key}').format( + key = item + )| safe }} +
    • + {% endfor %} +
    + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/license.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/license.html new file mode 100644 index 0000000..923d415 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/license.html @@ -0,0 +1,67 @@ +
  • +

    + {% set pkg_link = (h.url_for('dataset.read', id=change.pkg_id)) %} + {# if both of them have URLs #} + {% if change.new_url != "" and change.old_url != "" %} + + {{ _('Changed the license of ') }} + + {{ change.title }} + + + {{ _('to') }} + + {{change.new_title}} + + + {{ _('( previously') }} + + {{change.old_title}} + + {{_(')')}} + + {# if only the new one has a URL #} + {% elif change.new_url != "" and change.old_url == "" %} + + {{ _('Changed the license of') }} + + {{change.title}} + + + {{ _('to') }} + + {{change.new_title}} + + + {{ _('(previously') + change.old_title + ' )'}} + + {# if only the old one has a URL #} + {% elif change.new_url == "" and change.old_url != "" %} + + {{ _('Changed the license of') }} + + {{change.title}} + + + {{ _('to') + change.new_title }} + + {{ _('( previously') }} + + {{change.old_title}} + + {{_(')')}} + + {# otherwise neither has a URL #} + {% else %} + + {{ _('Changed the license of') }} + + {{change.title}} + + + {{ _('to') + change.new_title}} + {{ _('(previously') + change.old_title + _(')')|safe }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/maintainer.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/maintainer.html new file mode 100644 index 0000000..6a4ae58 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/maintainer.html @@ -0,0 +1,30 @@ +
  • +

    + {% if change.method == "change" %} + + {{ _('Set maintainer of {pkg_link} to {new_maintainer} (previously {old_maintainer})').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + new_maintainer = change.new_maintainer, + old_maintainer = change.old_maintainer + ) }} + + {% elif change.method == "add" %} + + {{ _('Set maintainer of {pkg_link} to {new_maintainer}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + new_maintainer = change.new_maintainer + ) }} + + {% elif change.method == "remove" %} + + {{ _('Removed maintainer from {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + ) }} + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/maintainer_email.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/maintainer_email.html new file mode 100644 index 0000000..a090cee --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/maintainer_email.html @@ -0,0 +1,47 @@ +
  • +

    + {% set pkg_link = (h.url_for('dataset.read', id=change.pkg_id)) %} + {% if change.method == "change" %} + + {{ _('Set maintainer email of') }} + + {{change.title}} + + + {{ _('to') }} + + {{change.new_maintainer_email}} + + + {{ _('(previously') }} + + {{change.old_maintainer_email}} + + {{ _(')') }} + + {% elif change.method == "add" %} + + {{ _('Set maintainer email of') }} + + {{change.title}} + + + {{ _('to') }} + + {{change.new_maintainer_email}} + + + {% elif change.method == "remove" %} + + {{ _('Removed maintainer email from') }} + + {{change.title}} + + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/name.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/name.html new file mode 100644 index 0000000..3ff43bd --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/name.html @@ -0,0 +1,23 @@ +
  • +

    + {% set old_url = h.url_for('dataset.read', qualified=True, id=change.old_name) %} + {% set new_url = h.url_for('dataset.read', qualified=True, id=change.new_name) %} + + {{ _('Moved')}} + + {{change.title}} + + + {{ _('from') }} + + {{old_url}} + + + {{ _('to') }} + + {{new_url}} + + + +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/new_file.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/new_file.html new file mode 100644 index 0000000..285d750 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/new_file.html @@ -0,0 +1,10 @@ +
  • +

    + {{ _('Uploaded a new file to resource {resource_link} in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', id=change.pkg_id, + resource_id=change.resource_id, qualified=True) + ) }} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/new_resource.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/new_resource.html new file mode 100644 index 0000000..0d29a68 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/new_resource.html @@ -0,0 +1,17 @@ +{% set dataset_type = request.view_args.package_type or pkg_dict['type'] or 'dataset' %} +{% set pkg_url = h.url_for(dataset_type ~ '.read', id=change.pkg_id) %} +{% set resource_url = h.url_for(dataset_type ~ '_resource.read', id=change.pkg_id, resource_id = change.resource_id, qualified=True) %} + +{% set pkg_link %} +{{ change.title }} +{% endset %} + +{% set resource_link %} +{{ change.resource_name or _('Unnamed resource') }} +{% endset %} + +
  • +

    + {{ _('Added resource {resource_link} to {pkg_link}').format(pkg_link=pkg_link, resource_link=resource_link) }} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/no_change.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/no_change.html new file mode 100644 index 0000000..0d1ae06 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/no_change.html @@ -0,0 +1,5 @@ +
  • +

    + {{ _('No fields were updated. See the metadata diff for more details.') }} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/notes.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/notes.html new file mode 100644 index 0000000..693da43 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/notes.html @@ -0,0 +1,30 @@ +
  • +

    + {% if change.method == "change" %} + + {{ _('Updated description of {pkg_link} from

    {old_notes}
    to
    {new_notes}
    ').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + old_notes = change.old_notes, + new_notes = change.new_notes + ) }} + + {% elif change.method == "add" %} + + {{ _('Updated description of {pkg_link} to
    {new_notes}
    ').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + new_notes = change.new_notes + ) }} + + {% elif change.method == "remove" %} + + {{ _('Removed description from {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id) + ) }} + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/org.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/org.html new file mode 100644 index 0000000..f215b30 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/org.html @@ -0,0 +1,30 @@ +
  • +

    + {% if change.method == "change" %} + + {{ _('Moved {pkg_link} from organization {old_org_link} to organization {new_org_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + old_org_link = h.nav_link(change.old_org_title, named_route='organization.read', id=change.old_org_id), + new_org_link = h.nav_link(change.new_org_title, named_route='organization.read', id=change.new_org_id) + ) }} + + {% elif change.method == "remove" %} + + {{ _('Removed {pkg_link} from organization {old_org_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + old_org_link = h.nav_link(change.old_org_title, named_route='organization.read', id=change.old_org_id) + ) }} + + {% elif change.method == "add" %} + + {{ _('Added {pkg_link} to organization {new_org_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + new_org_link = h.nav_link(change.new_org_title, named_route='organization.read', id=change.new_org_id) + ) }} + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/private.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/private.html new file mode 100644 index 0000000..377357a --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/private.html @@ -0,0 +1,8 @@ +
  • +

    + {{ _('Set visibility of {pkg_link} to {visibility}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + visibility = change.new + ) }} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_desc.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_desc.html new file mode 100644 index 0000000..62dc3c5 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_desc.html @@ -0,0 +1,39 @@ +
  • +

    + {% if change.method == "add" %} + + {{ _('Updated description of resource {resource_link} in {pkg_link} to

    {new_desc}
    ').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', id=change.pkg_id, + resource_id=change.resource_id, qualified=True), + new_desc = change.new_desc + ) }} + + {% elif change.method == "remove" %} + + {{ _('Removed description from resource {resource_link} in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', id=change.pkg_id, + resource_id=change.resource_id, qualified=True) + )}} + + {% elif change.method == "change" %} + + {{ _('Updated description of resource {resource_link} in {pkg_link} from
    {old_desc}
    to
    {new_desc}
    ').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', id=change.pkg_id, + resource_id=change.resource_id, qualified=True), + new_desc = change.new_desc, + old_desc = change.old_desc + ) }} + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_extras.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_extras.html new file mode 100644 index 0000000..8423361 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_extras.html @@ -0,0 +1,112 @@ +
  • +

    + {% if change.method == "add_one_value" %} + + {{ _('Added field {key} with value {value} to resource {resource_link} in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', id=change.pkg_id, + resource_id=change.resource_id, qualified=True), + key = change.key, + value = change.value + ) }} + + {% elif change.method == "add_one_no_value" %} + + {{ _('Added field {key} to resource {resource_link} in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', id=change.pkg_id, + resource_id=change.resource_id, qualified=True), + key = change.key, + ) }} + + {% elif change.method == "add_multiple" %} + + {{ _('Added the following fields to resource {resource_link} in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', id=change.pkg_id, + resource_id=change.resource_id, qualified=True) + ) }} +

      + {% for item in change.key_list %} + {% if change.value_list[item] != "" %} + {{ _('{key} with value {value}').format( + key = item, + value = change.value_list[item] + )|safe }} + {% else %} + {{ _('{key}').format( + key = item + )|safe }} + {% endif %} + {% endfor %} +
    + + {% elif change.method == "remove_one" %} + + {{ _('Removed field {key} from resource {resource_link} in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', id=change.pkg_id, + resource_id=change.resource_id, qualified=True), + key = change.key + ) }} + + {% elif change.method == "remove_multiple" %} + + {{ _('Removed the following fields from resource {resource_link} in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', id=change.pkg_id, + resource_id=change.resource_id, qualified=True) + ) }} +
      + {% for item in change.key_list %} + {{ _('{key}').format( + key = item + )|safe }} + {% endfor %} +
    + + {% elif change.method == "change_value_with_old" %} + + {{ _('Changed value of field {key} of resource {resource_link} to {new_val} (previously {old_val}) in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', id=change.pkg_id, + resource_id=change.resource_id, qualified=True), + key = change.key, + new_val = change.new_value, + old_val = change.old_value + ) }} + + {% elif change.method == "change_value_no_old" %} + + {{ _('Changed value of field {key} to {new_val} in resource {resource_link} in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', id=change.pkg_id, + resource_id=change.resource_id, qualified=True), + key = change.key, + new_val = change.new_value + ) }} + + {% elif change.method == "change_value_no_new" %} + + {{ _('Removed the value of field {key} in resource {resource_link} in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + resource_link = h.nav_link( + change.resource_name, named_route='resource.read', id=change.pkg_id, + resource_id=change.resource_id, qualified=True), + key = change.key, + new_val = change.new_value + ) }} + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_format.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_format.html new file mode 100644 index 0000000..d7ac855 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_format.html @@ -0,0 +1,54 @@ +
  • +

    + {% set format_search_base_url = ( + h.url_for("organization.read", id=change.org_id) + if change.org_id else + h.url_for("dataset.search")) %} + {% set resource_url = (h.url_for( + "resource.read", + id=change.pkg_id, + resource_id=change.resource_id, + qualified=True)) %} + + {% if change.method == "add" %} + + {{ _('Set format of resource') }} + + {{change.resource_name}} + + + {{ _('to') }} + + {{change.format}} + + + {{ _('in') }} + + {{change.title}} + + + {% elif change.method == "change" %} + + {{ _('Set format of resource') }} + + {{change.resource_name}} + + + {{ _('to') }} + + {{change.new_format}} + + + {{ _('(previously') }} + + {{change.old_format}} + + {{ _(')') }} + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_name.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_name.html new file mode 100644 index 0000000..a98a6be --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/resource_name.html @@ -0,0 +1,13 @@ +
  • +

    + {{ _('Renamed resource {old_resource_link} to {new_resource_link} in {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + old_resource_link = h.nav_link( + change.old_resource_name, named_route='resource.read', id=change.old_pkg_id, + resource_id=change.resource_id, activity_id=change.old_activity_id), + new_resource_link = h.nav_link( + change.new_resource_name, named_route='resource.read', id=change.new_pkg_id, + resource_id=change.resource_id), + ) }} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/tags.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/tags.html new file mode 100644 index 0000000..902a02a --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/tags.html @@ -0,0 +1,59 @@ +
  • +

    + {% if change.method == "remove_one" %} + + {{ _('Removed tag {tag_link} from {pkg_link}').format( + tag_link = '{tag}'.format( + tag_url = h.url_for('dataset.search') + + "?tags=" + change.tag, + tag = change.tag + )|safe, + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + ) }} + + {% elif change.method == "remove_multiple" %} + + {{ _('Removed the following tags from {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + )|safe }} + +

      + {% for item in change.tags %} +
    • + {{ _('{tag_link}').format( + tag_link = h.nav_link(item, named_route='dataset.search', tags=item), + )}} +
    • + {% endfor %} +
    + + {% elif change.method == "add_one" %} + + {{ _('Added tag {tag_link} to {pkg_link}').format( + tag_link = h.nav_link(change.tag, named_route='dataset.search', tags=change.tag), + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + ) }} + + {% elif change.method == "add_multiple" %} + + {{ _('Added the following tags to {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + ) }} +
      + {% for item in change.tags %} +
    • + {# TODO: figure out which controller to actually use here #} + {{ _('{tag_link}').format( + tag_link = h.nav_link(item, named_route='dataset.search', tags=item), + )}} +
    • + {% endfor %} +
    + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/title.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/title.html new file mode 100644 index 0000000..29623bb --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/title.html @@ -0,0 +1,8 @@ +
  • +

    + {{ _('Changed title to {title_link} (previously {old_title})').format( + title_link = h.nav_link(change.new_title, named_route='dataset.read', id=change.id), + old_title = change.old_title + ) }} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/url.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/url.html new file mode 100644 index 0000000..c194cd0 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/url.html @@ -0,0 +1,46 @@ +
  • +

    + {% set pkg_link = (h.url_for('dataset.read', id=change.pkg_id)) %} + {% if change.method == "change" %} + + {{ _('Changed the source URL of') }} + + {{change.title}} + + + {{ _('from') }} + + {{change.old_url}} + + + {{ _('to') }} + + {{change.new_url}} + + + {% elif change.method == "remove" %} + + {{ _('Removed the source URL from') }} + + {{change.title}} + + + {% elif change.method == "add" %} + + {{_('Changed the source URL of') }} + + {{change.title}} + + + {{ _('to') }} + + {{change.new_url}} + + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/version.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/version.html new file mode 100644 index 0000000..2c93434 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/changes/version.html @@ -0,0 +1,30 @@ +
  • +

    + {% if change.method == "change" %} + + {{ _('Changed the version of {pkg_link} to {new_version} (previously {old_version})').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + old_version = change.old_version, + new_version = change.new_version + ) }} + + {% elif change.method == "remove" %} + + {{ _('Removed the version from {pkg_link}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + ) }} + + {% elif change.method == "add" %} + + {{ _('Changed the version of {pkg_link} to {new_version}').format( + pkg_link = h.nav_link(change.title, named_route='dataset.read', id=change.pkg_id), + new_version = change.new_version + ) }} + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/description.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/description.html new file mode 100644 index 0000000..9210b6a --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/description.html @@ -0,0 +1,30 @@ +
  • +

    + {% if change.method == "change" %} + + {{ _('Updated description of {group_link} from

    {old_description}
    to
    {new_description}
    ').format( + group_link = h.nav_link(change.title, named_route='group.read', id=change.pkg_id), + old_description = change.old_description, + new_description = change.new_description + ) }} + + {% elif change.method == "add" %} + + {{ _('Updated description of {group_link} to
    {new_description}
    ').format( + group_link = h.nav_link(change.title, named_route='group.read', id=change.pkg_id), + new_description = change.new_description + ) }} + + {% elif change.method == "remove" %} + + {{ _('Removed description from {group_link}').format( + group_link = h.nav_link(change.title, named_route='group.read', id=change.pkg_id) + ) }} + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/image_url.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/image_url.html new file mode 100644 index 0000000..b74aabf --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/image_url.html @@ -0,0 +1,46 @@ +
  • +

    + {% set group_link = (h.url_for('group.read', id=change.pkg_id)) %} + {% if change.method == "change" %} + + {{ _('Changed the image URL of') }} + + {{change.title}} + + + {{ _('from') }} + + {{change.old_image_url}} + + + {{ _('to') }} + + {{change.new_image_url}} + + + {% elif change.method == "remove" %} + + {{ _('Removed the image URL from') }} + + {{change.title}} + + + {% elif change.method == "add" %} + + {{ _('Changed the image URL of') }} + + {{change.title}} + + + {{ _('to') }} + + {{change.new_image_url}} + + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/no_change.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/no_change.html new file mode 100644 index 0000000..0d1ae06 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/no_change.html @@ -0,0 +1,5 @@ +
  • +

    + {{ _('No fields were updated. See the metadata diff for more details.') }} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/title.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/title.html new file mode 100644 index 0000000..dc3bbdc --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/group_changes/title.html @@ -0,0 +1,8 @@ +
  • +

    + {{ _('Changed title to {title_link} (previously {old_title})').format( + title_link = h.nav_link(change.new_title, named_route='group.read', id=change.id), + old_title = change.old_title + ) }} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/description.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/description.html new file mode 100644 index 0000000..137f186 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/description.html @@ -0,0 +1,30 @@ +
  • +

    + {% if change.method == "change" %} + + {{ _('Updated description of {org_link} from

    {old_description}
    to
    {new_description}
    ').format( + org_link = h.nav_link(change.title, named_route='organization.read', id=change.pkg_id), + old_description = change.old_description, + new_description = change.new_description + ) }} + + {% elif change.method == "add" %} + + {{ _('Updated description of {org_link} to
    {new_description}
    ').format( + org_link = h.nav_link(change.title, named_route='organization.read', id=change.pkg_id), + new_description = change.new_description + ) }} + + {% elif change.method == "remove" %} + + {{ _('Removed description from {org_link}').format( + org_link = h.nav_link(change.title, named_route='organization.read', id=change.pkg_id) + ) }} + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/image_url.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/image_url.html new file mode 100644 index 0000000..5b9ce21 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/image_url.html @@ -0,0 +1,46 @@ +
  • +

    + {% set org_link = (h.url_for('organization.read', id=change.pkg_id)) %} + {% if change.method == "change" %} + + {{ _('Changed the image URL of') }} + + {{change.title}} + + + {{ _('from') }} + + {{change.old_image_url}} + + + {{ _('to') }} + + {{change.new_image_url}} + + + {% elif change.method == "remove" %} + + {{ _('Removed the image URL from') }} + + {{change.title}} + + + {% elif change.method == "add" %} + + {{ _('Changed the image URL of') }} + + {{change.title}} + + + {{ _('to') }} + + {{change.new_image_url}} + + + {% else %} + + {{ _('No fields were updated. See the metadata diff for more details.') }} + + {% endif %} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/no_change.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/no_change.html new file mode 100644 index 0000000..0d1ae06 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/no_change.html @@ -0,0 +1,5 @@ +
  • +

    + {{ _('No fields were updated. See the metadata diff for more details.') }} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/title.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/title.html new file mode 100644 index 0000000..444e142 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/organization_changes/title.html @@ -0,0 +1,8 @@ +
  • +

    + {{ _('Changed title to {title_link} (previously {old_title})').format( + title_link = h.nav_link(change.new_title, named_route='organization.read', id=change.id), + old_title = change.old_title + ) }} +

    +
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/pagination.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/pagination.html new file mode 100644 index 0000000..721daf1 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/pagination.html @@ -0,0 +1,7 @@ +{% set class_prev = "btn btn-default" if newer_activities_url else "btn disabled" %} +{% set class_next = "btn btn-default" if older_activities_url else "btn disabled" %} + + \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/stream.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/stream.html new file mode 100644 index 0000000..40dd2b4 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/snippets/stream.html @@ -0,0 +1,63 @@ +{% macro actor(activity) %} + + {{ h.linked_user(activity.user_id, 0, 30) }} + +{% endmacro %} + +{% macro dataset(activity) %} + {% set dataset_type = activity.data.package.type or 'dataset' %} + + + {{ activity.data.package.title if activity.data.package else _("unknown") }} + + +{% endmacro %} + +{% macro organization(activity) %} + {% set group_type = group_type or (activity.data.group.type if (activity.data.group and activity.data.group.type) else 'organization') %} + + {{ activity.data.group.title if activity.data.group else _('unknown') }} + +{% endmacro %} + +{% macro user(activity) %} + + {{ h.linked_user(activity.object_id, 0, 20) }} + +{% endmacro %} + +{% macro group(activity) %} + + + {{ activity.data.group.title if activity.data.group else _('unknown') }} + + +{% endmacro %} + +{# Renders the actual stream of activities + +activity_stream - the activity data. e.g. the output from package_activity_list +id - the id or current name of the object (e.g. package name, user id) +object_type - 'package', 'organization', 'group', 'user' + +#} +{% block activity_stream %} +
      + {% set can_show_activity_detail = h.check_access('activity_list', {'id': id, 'include_data': True, 'object_type': object_type}) %} + + {% for activity in activity_stream %} + {%- snippet "snippets/activities/{}.html".format(activity.activity_type.replace(' ', '_')), + "snippets/activities/fallback.html", + activity=activity, can_show_activity_detail=can_show_activity_detail, + ah={ + 'actor': actor, + 'dataset': dataset, + 'organization': organization, + 'user': user, + 'group': group, + }, + id=id + -%} + {% endfor %} +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/activity_stream.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/activity_stream.html new file mode 100644 index 0000000..ca6b5f8 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/activity_stream.html @@ -0,0 +1,14 @@ +{% extends "user/read.html" %} + +{% block subtitle %}{{ _('Activity Stream') }} {{ g.template_title_delimiter }} {{ super() }}{% endblock %} + +{% block primary_content_inner %} +

    + {% block page_heading -%} + {{ _('Activity Stream') }} + {%- endblock %} +

    + {% snippet 'snippets/stream.html', activity_stream=activity_stream, id=id, object_type='user' %} + + {% snippet 'snippets/pagination.html', newer_activities_url=newer_activities_url, older_activities_url=older_activities_url %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/dashboard.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/dashboard.html new file mode 100644 index 0000000..abb2d68 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/dashboard.html @@ -0,0 +1,27 @@ +{% ckan_extends %} + +{% block breadcrumb_content %} +
  • {{ _('Dashboard') }}
  • +{% endblock %} + +{% block dashboard_nav_links %} + {{ h.build_nav_icon('activity.dashboard', _('News feed'), icon='list') }} + {{ super() }} +{% endblock %} + + +{% block primary_content_inner %} +
    + {% snippet 'user/snippets/followee_dropdown.html', context=dashboard_activity_stream_context, followees=followee_list %} +

    + {% block page_heading %} + {{ _('News feed') }} + {% endblock %} + {{ _("Activity from items that I'm following") }} +

    + {% snippet 'snippets/stream.html', activity_stream=dashboard_activity_stream %} +
    + + {% snippet 'snippets/pagination.html', newer_activities_url=newer_activities_url, older_activities_url=older_activities_url %} + +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/edit_user_form.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/edit_user_form.html new file mode 100644 index 0000000..4747ef8 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/edit_user_form.html @@ -0,0 +1,12 @@ +{% ckan_extends %} + +{% block extra_fields %} + {% if h.activity_show_email_notifications() %} + {% call form.checkbox('activity_streams_email_notifications', label=_('Subscribe to notification emails'), id='field-activity-streams-email-notifications', value=True, checked=g.userobj.activity_streams_email_notifications) %} + {% set helper_text = _("You will receive notification emails from {site_title}, e.g. when you have new activities on your dashboard."|string) %} + {{ form.info(helper_text.format(site_title=g.site_title)) }} + {% endcall %} + {% endif %} + + {{ super() }} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/read_base.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/read_base.html new file mode 100644 index 0000000..dec036e --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/read_base.html @@ -0,0 +1,6 @@ +{% ckan_extends %} + +{% block content_primary_nav %} + {{ super() }} + {{ h.build_nav_icon('activity.user_activity', _('Activity Stream'), id=user.name, icon='clock') }} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/snippets/followee_dropdown.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/snippets/followee_dropdown.html new file mode 100644 index 0000000..f3db328 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/templates/user/snippets/followee_dropdown.html @@ -0,0 +1,52 @@ +{% macro followee_icon(type) -%} + {% if type == 'dataset' %} + + {% elif type == 'user' %} + + {% elif type == 'group' %} + + {% elif type == 'organization' %} + + {% endif %} +{%- endmacro %} + +
    + + +
    +
    +
    + + +
    +
    + {% if followees %} + + {% else %} +

    {{ _('You are not following anything') }}

    + {% endif %} +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/__init__.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/conftest.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/conftest.py new file mode 100644 index 0000000..11408a3 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/conftest.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +from pytest_factoryboy import register + +from ckan.tests.factories import CKANFactory +from ckanext.activity.model import Activity + + +@register +class ActivityFactory(CKANFactory): + """A factory class for creating CKAN activity objects.""" + + class Meta: + model = Activity + action = "activity_create" diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/__init__.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_action.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_action.py new file mode 100644 index 0000000..d32ed6b --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_action.py @@ -0,0 +1,2178 @@ +# -*- coding: utf-8 -*- + +import copy +import datetime +import time + +import pytest + +from ckan import model +import ckan.plugins.toolkit as tk + +import ckan.tests.helpers as helpers +import ckan.tests.factories as factories + +from ckanext.activity.model.activity import Activity, package_activity_list + + +def _clear_activities(): + from ckan import model + + model.Session.query(Activity).delete() + model.Session.flush() + + +def _seconds_since_timestamp(timestamp, format_): + dt = datetime.datetime.strptime(timestamp, format_) + now = datetime.datetime.utcnow() + assert now > dt # we assume timestamp is not in the future + return (now - dt).total_seconds() + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("with_plugins") +class TestLimits: + def test_activity_list_actions(self): + actions = [ + "user_activity_list", + "package_activity_list", + "group_activity_list", + "organization_activity_list", + "recently_changed_packages_activity_list", + "current_package_list_with_resources", + ] + for action in actions: + with pytest.raises(tk.ValidationError): + helpers.call_action( + action, + id="test_user", + limit="not_an_int", + offset="not_an_int", + ) + with pytest.raises(tk.ValidationError): + helpers.call_action( + action, id="test_user", limit=-1, offset=-1 + ) + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("non_clean_db", "with_plugins") +class TestActivityShow: + def test_simple_with_data(self, package, user, activity_factory): + activity = activity_factory( + user_id=user["id"], + object_id=package["id"], + activity_type="new package", + data={"package": copy.deepcopy(package), "actor": "Mr Someone"}, + ) + activity_shown = helpers.call_action( + "activity_show", id=activity["id"] + ) + assert activity_shown["user_id"] == user["id"] + assert ( + _seconds_since_timestamp( + activity_shown["timestamp"], "%Y-%m-%dT%H:%M:%S.%f" + ) + < 10 + ) + assert activity_shown["object_id"] == package["id"] + assert activity_shown["data"] == { + "package": package, + "actor": "Mr Someone", + } + assert activity_shown["activity_type"] == "new package" + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("clean_db", "with_plugins") +class TestPackageActivityList(object): + def test_create_dataset(self): + user = factories.User() + dataset = factories.Dataset(user=user) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "new package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + assert "extras" in activities[0]["data"]["package"] + + def test_change_dataset(self): + user = factories.User() + _clear_activities() + dataset = factories.Dataset(user=user) + original_title = dataset["title"] + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package", + "new package", + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + assert ( + activities[0]["data"]["package"]["title"] + == "Dataset with changed title" + ) + + # the old dataset still has the old title + assert activities[1]["activity_type"] == "new package" + assert activities[1]["data"]["package"]["title"] == original_title + + def test_change_dataset_add_extra(self): + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + dataset["extras"].append(dict(key="rating", value="great")) + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + assert "extras" in activities[0]["data"]["package"] + + def test_change_dataset_change_extra(self): + user = factories.User() + dataset = factories.Dataset( + user=user, extras=[dict(key="rating", value="great")] + ) + _clear_activities() + dataset["extras"][0] = dict(key="rating", value="ok") + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + assert "extras" in activities[0]["data"]["package"] + + def test_change_dataset_delete_extra(self): + user = factories.User() + dataset = factories.Dataset( + user=user, extras=[dict(key="rating", value="great")] + ) + _clear_activities() + dataset["extras"] = [] + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + assert "extras" in activities[0]["data"]["package"] + + def test_change_dataset_add_resource(self): + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + factories.Resource(package_id=dataset["id"], user=user) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + # NB the detail is not included - that is only added in by + # activity_list_to_html() + + def test_change_dataset_change_resource(self): + user = factories.User() + dataset = factories.Dataset( + user=user, + resources=[dict(url="https://example.com/foo.csv", format="csv")], + ) + _clear_activities() + dataset["resources"][0]["format"] = "pdf" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_change_dataset_delete_resource(self): + user = factories.User() + dataset = factories.Dataset( + user=user, + resources=[dict(url="https://example.com/foo.csv", format="csv")], + ) + _clear_activities() + dataset["resources"] = [] + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_change_dataset_add_tag(self): + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + dataset["tags"].append(dict(name="checked")) + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_delete_tag_from_dataset(self): + user = factories.User() + dataset = factories.Dataset(user=user, tags=[dict(name="checked")]) + _clear_activities() + dataset["tags"] = [] + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_delete_dataset(self): + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + helpers.call_action( + "package_delete", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "deleted package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_private_dataset_has_no_activity(self): + user = factories.User() + org = factories.Organization(user=user) + _clear_activities() + dataset = factories.Dataset( + private=True, owner_org=org["id"], user=user + ) + dataset["tags"] = [] + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [] + + def test_private_dataset_delete_has_no_activity(self): + user = factories.User() + org = factories.Organization(user=user) + _clear_activities() + dataset = factories.Dataset( + private=True, owner_org=org["id"], user=user + ) + helpers.call_action( + "package_delete", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [] + + def _create_bulk_types_activities(self, types): + dataset = factories.Dataset() + from ckan import model + + user = factories.User() + + objs = [ + Activity( + user_id=user["id"], + object_id=dataset["id"], + activity_type=activity_type, + data=None, + ) + for activity_type in types + ] + model.Session.add_all(objs) + model.repo.commit_and_remove() + return dataset["id"] + + def test_error_bad_search(self): + with pytest.raises(tk.ValidationError): + helpers.call_action( + "package_activity_list", + id=id, + activity_types=["new package"], + exclude_activity_types=["deleted package"], + ) + + def test_activity_types_filter(self): + types = [ + "new package", + "changed package", + "deleted package", + "changed package", + "new package", + ] + id = self._create_bulk_types_activities(types) + + activities_new = helpers.call_action( + "package_activity_list", id=id, activity_types=["new package"] + ) + assert len(activities_new) == 2 + + activities_not_new = helpers.call_action( + "package_activity_list", + id=id, + exclude_activity_types=["new package"], + ) + assert len(activities_not_new) == 3 + + activities_delete = helpers.call_action( + "package_activity_list", id=id, activity_types=["deleted package"] + ) + assert len(activities_delete) == 1 + + activities_not_deleted = helpers.call_action( + "package_activity_list", + id=id, + exclude_activity_types=["deleted package"], + ) + assert len(activities_not_deleted) == 4 + + def _create_bulk_package_activities(self, count): + dataset = factories.Dataset() + from ckan import model + + user = factories.User() + + objs = [ + Activity( + user_id=user["id"], + object_id=dataset["id"], + activity_type=None, + data=None, + ) + for _ in range(count) + ] + model.Session.add_all(objs) + model.repo.commit_and_remove() + return dataset["id"] + + def test_limit_default(self): + id = self._create_bulk_package_activities(35) + results = helpers.call_action("package_activity_list", id=id) + assert len(results) == 31 # i.e. default value + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + def test_limit_configured(self): + id = self._create_bulk_package_activities(7) + results = helpers.call_action("package_activity_list", id=id) + assert len(results) == 5 # i.e. ckan.activity_list_limit + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + @pytest.mark.ckan_config("ckan.activity_list_limit_max", "7") + def test_limit_hits_max(self): + id = self._create_bulk_package_activities(9) + results = helpers.call_action( + "package_activity_list", id=id, limit="9" + ) + assert len(results) == 7 # i.e. ckan.activity_list_limit_max + + def test_normal_user_doesnt_see_hidden_activities(self): + # activity is 'hidden' because dataset is created by site_user + dataset = factories.Dataset() + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [] + + def test_sysadmin_user_doesnt_see_hidden_activities_by_default(self): + # activity is 'hidden' because dataset is created by site_user + dataset = factories.Dataset() + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + assert [activity["activity_type"] for activity in activities] == [] + + def test_sysadmin_user_can_include_hidden_activities(self): + # activity is 'hidden' because dataset is created by site_user + dataset = factories.Dataset() + + activities = helpers.call_action( + "package_activity_list", + include_hidden_activity=True, + id=dataset["id"], + ) + assert [activity["activity_type"] for activity in activities] == [ + "new package" + ] + + def _create_dataset_with_activities(self, updates: int = 3): + user = factories.User() + dataset = factories.Dataset(user=user) + ctx = {"user": user["name"]} + + for c in range(updates): + dataset["title"] = "Dataset v{}".format(c) + helpers.call_action("package_update", context=ctx, **dataset) + + return dataset + + def test_activity_after(self): + """Test activities after timestamp""" + dataset = self._create_dataset_with_activities() + + db_activities = package_activity_list(dataset["id"], limit=10) + pkg_activities = helpers.call_action( + "package_activity_list", + id=dataset["id"], + after=db_activities[2].timestamp.timestamp(), + ) + # we expect just 2 (the first 2) + assert len(pkg_activities) == 2 + # first activity here is the first one. + assert pkg_activities[0]["activity_type"] == "changed package" + pkg_activity_time = datetime.datetime.fromisoformat( + pkg_activities[0]["timestamp"] + ) + assert pkg_activity_time == db_activities[0].timestamp + + # last activity here is the 2nd one. + assert pkg_activities[1]["activity_type"] == "changed package" + pkg_activity_time = datetime.datetime.fromisoformat( + pkg_activities[1]["timestamp"] + ) + assert pkg_activity_time == db_activities[1].timestamp + + def test_activity_offset(self): + """Test activities after timestamp""" + dataset = self._create_dataset_with_activities() + + db_activities = package_activity_list(dataset["id"], limit=10) + pkg_activities = helpers.call_action( + "package_activity_list", id=dataset["id"], offset=2 + ) + # we expect just 2 (the last 2) + assert len(pkg_activities) == 2 + # first activity here is the first one. + assert pkg_activities[0]["activity_type"] == "changed package" + pkg_activity_time = datetime.datetime.fromisoformat( + pkg_activities[0]["timestamp"] + ) + assert pkg_activity_time == db_activities[2].timestamp + + # last activity here is the package creation. + assert pkg_activities[1]["activity_type"] == "new package" + pkg_activity_time = datetime.datetime.fromisoformat( + pkg_activities[1]["timestamp"] + ) + assert pkg_activity_time == db_activities[3].timestamp + + def test_activity_before(self): + """Test activities before timestamp""" + dataset = self._create_dataset_with_activities() + + db_activities = package_activity_list(dataset["id"], limit=10) + pkg_activities = helpers.call_action( + "package_activity_list", + id=dataset["id"], + before=db_activities[1].timestamp.timestamp(), + ) + # we expect just 2 (the last 2) + assert len(pkg_activities) == 2 + # first activity here is the first one. + assert pkg_activities[0]["activity_type"] == "changed package" + pkg_activity_time = datetime.datetime.fromisoformat( + pkg_activities[0]["timestamp"] + ) + assert pkg_activity_time == db_activities[2].timestamp + + # last activity here is the package creation. + assert pkg_activities[-1]["activity_type"] == "new package" + pkg_activity_time = datetime.datetime.fromisoformat( + pkg_activities[-1]["timestamp"] + ) + assert pkg_activity_time == db_activities[3].timestamp + + def test_activity_after_before(self): + """Test activities before timestamp""" + dataset = self._create_dataset_with_activities() + + db_activities = package_activity_list(dataset["id"], limit=10) + pkg_activities = helpers.call_action( + "package_activity_list", + id=dataset["id"], + before=db_activities[1].timestamp.timestamp(), + after=db_activities[3].timestamp.timestamp(), + ) + # we expect just 1 (db_activities[2]) + assert len(pkg_activities) == 1 + # first activity here is the first one. + assert pkg_activities[0]["activity_type"] == "changed package" + pkg_activity_time = datetime.datetime.fromisoformat( + pkg_activities[0]["timestamp"] + ) + assert pkg_activity_time == db_activities[2].timestamp + + def test_activity_after_before_offset(self): + """Test activities before timestamp""" + dataset = self._create_dataset_with_activities(updates=4) + + db_activities = package_activity_list(dataset["id"], limit=10) + pkg_activities = helpers.call_action( + "package_activity_list", + id=dataset["id"], + before=db_activities[1].timestamp.timestamp(), + after=db_activities[4].timestamp.timestamp(), + offset=1, + ) + # we expect just 1 (db_activities[3]) + assert len(pkg_activities) == 1 + # first activity here is the first one. + assert pkg_activities[0]["activity_type"] == "changed package" + pkg_activity_time = datetime.datetime.fromisoformat( + pkg_activities[0]["timestamp"] + ) + assert pkg_activity_time == db_activities[3].timestamp + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("clean_db", "with_plugins") +class TestUserActivityList(object): + def test_create_user(self): + user = factories.User() + + activities = helpers.call_action("user_activity_list", id=user["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "new user" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == user["id"] + + def test_user_update_activity_stream(self): + """Test that the right activity is emitted when updating a user.""" + + user = factories.User() + before = datetime.datetime.utcnow() + + # FIXME we have to pass the email address and password to user_update + # even though we're not updating those fields, otherwise validation + # fails. + helpers.call_action( + "user_update", + id=user["id"], + name=user["name"], + email=user["email"], + password=factories.User.stub().password, + fullname="updated full name", + ) + + activity_stream = helpers.call_action( + "user_activity_list", id=user["id"] + ) + latest_activity = activity_stream[0] + assert latest_activity["activity_type"] == "changed user" + assert latest_activity["object_id"] == user["id"] + assert latest_activity["user_id"] == user["id"] + after = datetime.datetime.utcnow() + timestamp = datetime.datetime.strptime( + latest_activity["timestamp"], "%Y-%m-%dT%H:%M:%S.%f" + ) + assert timestamp >= before and timestamp <= after + + def test_create_dataset(self): + user = factories.User() + _clear_activities() + dataset = factories.Dataset(user=user) + + activities = helpers.call_action("user_activity_list", id=user["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "new package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_dataset_changed_by_another_user(self): + user = factories.User() + another_user = factories.Sysadmin() + dataset = factories.Dataset(user=user) + _clear_activities() + dataset["extras"].append(dict(key="rating", value="great")) + helpers.call_action( + "package_update", context={"user": another_user["name"]}, **dataset + ) + + # the user might have created the dataset, but a change by another + # user does not show on the user's activity stream + activities = helpers.call_action("user_activity_list", id=user["id"]) + assert [activity["activity_type"] for activity in activities] == [] + + def test_change_dataset_add_extra(self): + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + dataset["extras"].append(dict(key="rating", value="great")) + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action("user_activity_list", id=user["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_change_dataset_add_tag(self): + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + dataset["tags"].append(dict(name="checked")) + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action("user_activity_list", id=user["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_create_group(self): + user = factories.User() + _clear_activities() + group = factories.Group(user=user) + + activities = helpers.call_action("user_activity_list", id=user["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "new group" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == group["id"] + assert activities[0]["data"]["group"]["title"] == group["title"] + + def test_delete_group_using_group_delete(self): + user = factories.User() + group = factories.Group(user=user) + _clear_activities() + helpers.call_action( + "group_delete", context={"user": user["name"]}, **group + ) + + activities = helpers.call_action("user_activity_list", id=user["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "deleted group" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == group["id"] + assert activities[0]["data"]["group"]["title"] == group["title"] + + def test_delete_group_by_updating_state(self): + user = factories.User() + group = factories.Group(user=user) + _clear_activities() + group["state"] = "deleted" + helpers.call_action( + "group_update", context={"user": user["name"]}, **group + ) + + activities = helpers.call_action("user_activity_list", id=user["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "deleted group" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == group["id"] + assert activities[0]["data"]["group"]["title"] == group["title"] + + def test_create_organization(self): + user = factories.User() + _clear_activities() + org = factories.Organization(user=user) + + activities = helpers.call_action("user_activity_list", id=user["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "new organization" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == org["id"] + assert activities[0]["data"]["group"]["title"] == org["title"] + + def test_delete_org_using_organization_delete(self): + user = factories.User() + org = factories.Organization(user=user) + _clear_activities() + helpers.call_action( + "organization_delete", context={"user": user["name"]}, **org + ) + + activities = helpers.call_action("user_activity_list", id=user["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "deleted organization" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == org["id"] + assert activities[0]["data"]["group"]["title"] == org["title"] + + def test_delete_org_by_updating_state(self): + user = factories.User() + org = factories.Organization(user=user) + _clear_activities() + org["state"] = "deleted" + helpers.call_action( + "organization_update", context={"user": user["name"]}, **org + ) + + activities = helpers.call_action("user_activity_list", id=user["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "deleted organization" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == org["id"] + assert activities[0]["data"]["group"]["title"] == org["title"] + + def _create_bulk_user_activities(self, count): + from ckan import model + + user = factories.User() + + objs = [ + Activity( + user_id=user["id"], + object_id=None, + activity_type=None, + data=None, + ) + for _ in range(count) + ] + model.Session.add_all(objs) + model.repo.commit_and_remove() + return user["id"] + + def test_limit_default(self): + id = self._create_bulk_user_activities(35) + results = helpers.call_action("user_activity_list", id=id) + assert len(results) == 31 # i.e. default value + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + def test_limit_configured(self): + id = self._create_bulk_user_activities(7) + results = helpers.call_action("user_activity_list", id=id) + assert len(results) == 5 # i.e. ckan.activity_list_limit + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + @pytest.mark.ckan_config("ckan.activity_list_limit_max", "7") + def test_limit_hits_max(self): + id = self._create_bulk_user_activities(9) + results = helpers.call_action("user_activity_list", id=id, limit="9") + assert len(results) == 7 # i.e. ckan.activity_list_limit_max + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("clean_db", "with_plugins") +class TestGroupActivityList(object): + def test_create_group(self): + user = factories.User() + group = factories.Group(user=user) + + activities = helpers.call_action("group_activity_list", id=group["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "new group" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == group["id"] + assert activities[0]["data"]["group"]["title"] == group["title"] + + def test_change_group(self): + user = factories.User() + _clear_activities() + group = factories.Group(user=user) + original_title = group["title"] + group["title"] = "Group with changed title" + helpers.call_action( + "group_update", context={"user": user["name"]}, **group + ) + + activities = helpers.call_action("group_activity_list", id=group["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "changed group", + "new group", + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == group["id"] + assert ( + activities[0]["data"]["group"]["title"] + == "Group with changed title" + ) + + # the old group still has the old title + assert activities[1]["activity_type"] == "new group" + assert activities[1]["data"]["group"]["title"] == original_title + + def test_create_dataset(self): + user = factories.User() + group = factories.Group(user=user) + _clear_activities() + dataset = factories.Dataset(groups=[{"id": group["id"]}], user=user) + + activities = helpers.call_action("group_activity_list", id=group["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "new package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_change_dataset(self): + user = factories.User() + group = factories.Group(user=user) + _clear_activities() + dataset = factories.Dataset(groups=[{"id": group["id"]}], user=user) + original_title = dataset["title"] + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action("group_activity_list", id=group["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "changed package", + "new package", + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + # the old dataset still has the old title + assert activities[1]["activity_type"] == "new package" + assert activities[1]["data"]["package"]["title"] == original_title + + def test_change_dataset_add_extra(self): + user = factories.User() + group = factories.Group(user=user) + dataset = factories.Dataset(groups=[{"id": group["id"]}], user=user) + _clear_activities() + dataset["extras"].append(dict(key="rating", value="great")) + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action("group_activity_list", id=group["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_change_dataset_add_tag(self): + user = factories.User() + group = factories.Group(user=user) + dataset = factories.Dataset(groups=[{"id": group["id"]}], user=user) + _clear_activities() + dataset["tags"].append(dict(name="checked")) + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action("group_activity_list", id=group["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_delete_dataset(self): + user = factories.User() + group = factories.Group(user=user) + dataset = factories.Dataset(groups=[{"id": group["id"]}], user=user) + _clear_activities() + helpers.call_action( + "package_delete", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action("group_activity_list", id=group["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "deleted package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_change_dataset_that_used_to_be_in_the_group(self): + user = factories.User() + group = factories.Group(user=user) + dataset = factories.Dataset(groups=[{"id": group["id"]}], user=user) + # remove the dataset from the group + dataset["groups"] = [] + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + _clear_activities() + # edit the dataset + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + # dataset change should not show up in its former group + activities = helpers.call_action("group_activity_list", id=group["id"]) + assert [activity["activity_type"] for activity in activities] == [] + + def test_delete_dataset_that_used_to_be_in_the_group(self): + user = factories.User() + group = factories.Group(user=user) + dataset = factories.Dataset(groups=[{"id": group["id"]}], user=user) + # remove the dataset from the group + dataset["groups"] = [] + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + _clear_activities() + helpers.call_action( + "package_delete", context={"user": user["name"]}, **dataset + ) + + # NOTE: + # ideally the dataset's deletion would not show up in its old group + # but it can't be helped without _group_activity_query getting very + # complicated + activities = helpers.call_action("group_activity_list", id=group["id"]) + assert [activity["activity_type"] for activity in activities] == [ + "deleted package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def _create_bulk_group_activities(self, count): + group = factories.Group() + from ckan import model + + user = factories.User() + + objs = [ + Activity( + user_id=user["id"], + object_id=group["id"], + activity_type=None, + data=None, + ) + for _ in range(count) + ] + model.Session.add_all(objs) + model.repo.commit_and_remove() + return group["id"] + + def test_limit_default(self): + id = self._create_bulk_group_activities(35) + results = helpers.call_action("group_activity_list", id=id) + assert len(results) == 31 # i.e. default value + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + def test_limit_configured(self): + id = self._create_bulk_group_activities(7) + results = helpers.call_action("group_activity_list", id=id) + assert len(results) == 5 # i.e. ckan.activity_list_limit + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + @pytest.mark.ckan_config("ckan.activity_list_limit_max", "7") + def test_limit_hits_max(self): + id = self._create_bulk_group_activities(9) + results = helpers.call_action("group_activity_list", id=id, limit="9") + assert len(results) == 7 # i.e. ckan.activity_list_limit_max + + def test_normal_user_doesnt_see_hidden_activities(self): + # activity is 'hidden' because group is created by site_user + group = factories.Group() + + activities = helpers.call_action("group_activity_list", id=group["id"]) + assert [activity["activity_type"] for activity in activities] == [] + + def test_sysadmin_user_doesnt_see_hidden_activities_by_default(self): + # activity is 'hidden' because group is created by site_user + group = factories.Group() + + activities = helpers.call_action("group_activity_list", id=group["id"]) + assert [activity["activity_type"] for activity in activities] == [] + + def test_sysadmin_user_can_include_hidden_activities(self): + # activity is 'hidden' because group is created by site_user + group = factories.Group() + + activities = helpers.call_action( + "group_activity_list", include_hidden_activity=True, id=group["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "new group" + ] + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("clean_db", "with_plugins") +class TestOrganizationActivityList(object): + def test_bulk_make_public(self): + org = factories.Organization() + + dataset1 = factories.Dataset(owner_org=org["id"], private=True) + dataset2 = factories.Dataset(owner_org=org["id"], private=True) + + helpers.call_action( + "bulk_update_public", + {}, + datasets=[dataset1["id"], dataset2["id"]], + org_id=org["id"], + ) + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert activities[0]["activity_type"] == "changed package" + + def test_bulk_delete(self): + org = factories.Organization() + + dataset1 = factories.Dataset(owner_org=org["id"]) + dataset2 = factories.Dataset(owner_org=org["id"]) + + helpers.call_action( + "bulk_update_delete", + {}, + datasets=[dataset1["id"], dataset2["id"]], + org_id=org["id"], + ) + + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert activities[0]["activity_type"] == "deleted package" + + def test_create_organization(self): + user = factories.User() + org = factories.Organization(user=user) + + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "new organization" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == org["id"] + assert activities[0]["data"]["group"]["title"] == org["title"] + + def test_change_organization(self): + user = factories.User() + _clear_activities() + org = factories.Organization(user=user) + original_title = org["title"] + org["title"] = "Organization with changed title" + helpers.call_action( + "organization_update", context={"user": user["name"]}, **org + ) + + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed organization", + "new organization", + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == org["id"] + assert ( + activities[0]["data"]["group"]["title"] + == "Organization with changed title" + ) + + # the old org still has the old title + assert activities[1]["activity_type"] == "new organization" + assert activities[1]["data"]["group"]["title"] == original_title + + def test_create_dataset(self): + user = factories.User() + org = factories.Organization(user=user) + _clear_activities() + dataset = factories.Dataset(owner_org=org["id"], user=user) + + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "new package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_change_dataset(self): + user = factories.User() + org = factories.Organization(user=user) + _clear_activities() + dataset = factories.Dataset(owner_org=org["id"], user=user) + original_title = dataset["title"] + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package", + "new package", + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + # the old dataset still has the old title + assert activities[1]["activity_type"] == "new package" + assert activities[1]["data"]["package"]["title"] == original_title + + def test_change_dataset_add_tag(self): + user = factories.User() + org = factories.Organization(user=user) + dataset = factories.Dataset(owner_org=org["id"], user=user) + _clear_activities() + dataset["tags"].append(dict(name="checked")) + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_delete_dataset(self): + user = factories.User() + org = factories.Organization(user=user) + dataset = factories.Dataset(owner_org=org["id"], user=user) + _clear_activities() + helpers.call_action( + "package_delete", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "deleted package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_change_dataset_that_used_to_be_in_the_org(self): + user = factories.User() + org = factories.Organization(user=user) + org2 = factories.Organization(user=user) + dataset = factories.Dataset(owner_org=org["id"], user=user) + # remove the dataset from the org + dataset["owner_org"] = org2["id"] + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + _clear_activities() + # edit the dataset + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + # dataset change should not show up in its former group + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [] + + def test_delete_dataset_that_used_to_be_in_the_org(self): + user = factories.User() + org = factories.Organization(user=user) + org2 = factories.Organization(user=user) + dataset = factories.Dataset(owner_org=org["id"], user=user) + # remove the dataset from the group + dataset["owner_org"] = org2["id"] + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + _clear_activities() + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_delete", context={"user": user["name"]}, **dataset + ) + + # dataset deletion should not show up in its former org + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [] + + def _create_bulk_org_activities(self, count): + org = factories.Organization() + from ckan import model + + user = factories.User() + + objs = [ + Activity( + user_id=user["id"], + object_id=org["id"], + activity_type=None, + data=None, + ) + for _ in range(count) + ] + model.Session.add_all(objs) + model.repo.commit_and_remove() + return org["id"] + + def test_limit_default(self): + id = self._create_bulk_org_activities(35) + results = helpers.call_action("organization_activity_list", id=id) + assert len(results) == 31 # i.e. default value + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + def test_limit_configured(self): + id = self._create_bulk_org_activities(7) + results = helpers.call_action("organization_activity_list", id=id) + assert len(results) == 5 # i.e. ckan.activity_list_limit + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + @pytest.mark.ckan_config("ckan.activity_list_limit_max", "7") + def test_limit_hits_max(self): + id = self._create_bulk_org_activities(9) + results = helpers.call_action( + "organization_activity_list", id=id, limit="9" + ) + assert len(results) == 7 # i.e. ckan.activity_list_limit_max + + def test_normal_user_doesnt_see_hidden_activities(self): + # activity is 'hidden' because org is created by site_user + org = factories.Organization() + + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [] + + def test_sysadmin_user_doesnt_see_hidden_activities_by_default(self): + # activity is 'hidden' because org is created by site_user + org = factories.Organization() + + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [] + + def test_sysadmin_user_can_include_hidden_activities(self): + # activity is 'hidden' because org is created by site_user + org = factories.Organization() + + activities = helpers.call_action( + "organization_activity_list", + include_hidden_activity=True, + id=org["id"], + ) + assert [activity["activity_type"] for activity in activities] == [ + "new organization" + ] + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("clean_db", "with_plugins") +class TestRecentlyChangedPackagesActivityList: + def test_create_dataset(self): + user = factories.User() + org = factories.Dataset(user=user) + + activities = helpers.call_action( + "recently_changed_packages_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "new package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == org["id"] + assert activities[0]["data"]["package"]["title"] == org["title"] + + def test_change_dataset(self): + user = factories.User() + org = factories.Organization(user=user) + _clear_activities() + dataset = factories.Dataset(owner_org=org["id"], user=user) + original_title = dataset["title"] + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "recently_changed_packages_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package", + "new package", + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + # the old dataset still has the old title + assert activities[1]["activity_type"] == "new package" + assert activities[1]["data"]["package"]["title"] == original_title + + def test_change_dataset_add_extra(self): + user = factories.User() + org = factories.Organization(user=user) + dataset = factories.Dataset(owner_org=org["id"], user=user) + _clear_activities() + dataset["extras"].append(dict(key="rating", value="great")) + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "recently_changed_packages_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_change_dataset_add_tag(self): + user = factories.User() + org = factories.Organization(user=user) + dataset = factories.Dataset(owner_org=org["id"], user=user) + _clear_activities() + dataset["tags"].append(dict(name="checked")) + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "recently_changed_packages_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "changed package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def test_delete_dataset(self): + user = factories.User() + org = factories.Organization(user=user) + dataset = factories.Dataset(owner_org=org["id"], user=user) + _clear_activities() + helpers.call_action( + "package_delete", context={"user": user["name"]}, **dataset + ) + + activities = helpers.call_action( + "organization_activity_list", id=org["id"] + ) + assert [activity["activity_type"] for activity in activities] == [ + "deleted package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + + def _create_bulk_package_activities(self, count): + from ckan import model + + user = factories.User() + + objs = [ + Activity( + user_id=user["id"], + object_id=None, + activity_type="new_package", + data=None, + ) + for _ in range(count) + ] + model.Session.add_all(objs) + model.repo.commit_and_remove() + + def test_limit_default(self): + self._create_bulk_package_activities(35) + results = helpers.call_action( + "recently_changed_packages_activity_list" + ) + assert len(results) == 31 # i.e. default value + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + def test_limit_configured(self): + self._create_bulk_package_activities(7) + results = helpers.call_action( + "recently_changed_packages_activity_list" + ) + assert len(results) == 5 # i.e. ckan.activity_list_limit + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + @pytest.mark.ckan_config("ckan.activity_list_limit_max", "7") + def test_limit_hits_max(self): + self._create_bulk_package_activities(9) + results = helpers.call_action( + "recently_changed_packages_activity_list", limit="9" + ) + assert len(results) == 7 # i.e. ckan.activity_list_limit_max + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("clean_db", "with_plugins") +class TestDashboardActivityList(object): + def test_create_user(self): + user = factories.User() + + activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [activity["activity_type"] for activity in activities] == [ + "new user" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == user["id"] + # user's own activities are always marked ``'is_new': False`` + assert not activities[0]["is_new"] + + def test_create_dataset(self): + user = factories.User() + _clear_activities() + dataset = factories.Dataset(user=user) + + activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [activity["activity_type"] for activity in activities] == [ + "new package" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == dataset["id"] + assert activities[0]["data"]["package"]["title"] == dataset["title"] + # user's own activities are always marked ``'is_new': False`` + assert not activities[0]["is_new"] + + def test_create_group(self): + user = factories.User() + _clear_activities() + group = factories.Group(user=user) + + activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [activity["activity_type"] for activity in activities] == [ + "new group" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == group["id"] + assert activities[0]["data"]["group"]["title"] == group["title"] + # user's own activities are always marked ``'is_new': False`` + assert not activities[0]["is_new"] + + def test_create_organization(self): + user = factories.User() + _clear_activities() + org = factories.Organization(user=user) + + activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [activity["activity_type"] for activity in activities] == [ + "new organization" + ] + assert activities[0]["user_id"] == user["id"] + assert activities[0]["object_id"] == org["id"] + assert activities[0]["data"]["group"]["title"] == org["title"] + # user's own activities are always marked ``'is_new': False`` + assert not activities[0]["is_new"] + + def _create_bulk_package_activities(self, count): + user = factories.User() + from ckan import model + + objs = [ + Activity( + user_id=user["id"], + object_id=None, + activity_type=None, + data=None, + ) + for _ in range(count) + ] + model.Session.add_all(objs) + model.repo.commit_and_remove() + return user["id"] + + def test_limit_default(self): + id = self._create_bulk_package_activities(35) + results = helpers.call_action( + "dashboard_activity_list", context={"user": id} + ) + assert len(results) == 31 # i.e. default value + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + def test_limit_configured(self): + id = self._create_bulk_package_activities(7) + results = helpers.call_action( + "dashboard_activity_list", context={"user": id} + ) + assert len(results) == 5 # i.e. ckan.activity_list_limit + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + @pytest.mark.ckan_config("ckan.activity_list_limit_max", "7") + def test_limit_hits_max(self): + id = self._create_bulk_package_activities(9) + results = helpers.call_action( + "dashboard_activity_list", limit="9", context={"user": id} + ) + assert len(results) == 7 # i.e. ckan.activity_list_limit_max + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("clean_db", "with_plugins") +class TestDashboardNewActivities(object): + def test_users_own_activities(self): + # a user's own activities are not shown as "new" + user = factories.User() + dataset = factories.Dataset(user=user) + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + helpers.call_action( + "package_delete", context={"user": user["name"]}, **dataset + ) + group = factories.Group(user=user) + group["title"] = "Group with changed title" + helpers.call_action( + "group_update", context={"user": user["name"]}, **group + ) + helpers.call_action( + "group_delete", context={"user": user["name"]}, **group + ) + + new_activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [activity["is_new"] for activity in new_activities] == [ + False + ] * 7 + new_activities_count = helpers.call_action( + "dashboard_new_activities_count", context={"user": user["id"]} + ) + assert new_activities_count == 0 + + def test_activities_by_a_followed_user(self): + user = factories.User() + followed_user = factories.User() + helpers.call_action( + "follow_user", context={"user": user["name"]}, **followed_user + ) + _clear_activities() + dataset = factories.Dataset(user=followed_user) + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", + context={"user": followed_user["name"]}, + **dataset, + ) + helpers.call_action( + "package_delete", + context={"user": followed_user["name"]}, + **dataset, + ) + group = factories.Group(user=followed_user) + group["title"] = "Group with changed title" + helpers.call_action( + "group_update", context={"user": followed_user["name"]}, **group + ) + helpers.call_action( + "group_delete", context={"user": followed_user["name"]}, **group + ) + + activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [ + activity["activity_type"] for activity in activities[::-1] + ] == [ + "new package", + "changed package", + "deleted package", + "new group", + "changed group", + "deleted group", + ] + assert [activity["is_new"] for activity in activities] == [True] * 6 + assert ( + helpers.call_action( + "dashboard_new_activities_count", context={"user": user["id"]} + ) + == 6 + ) + + def test_activities_on_a_followed_dataset(self): + user = factories.User() + another_user = factories.Sysadmin() + _clear_activities() + dataset = factories.Dataset(user=another_user) + helpers.call_action( + "follow_dataset", context={"user": user["name"]}, **dataset + ) + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": another_user["name"]}, **dataset + ) + + activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [ + (activity["activity_type"], activity["is_new"]) + for activity in activities[::-1] + ] == [ + ("new package", True), + # NB The 'new package' activity is in our activity stream and shows + # as "new" even though it occurred before we followed it. This is + # known & intended design. + ("changed package", True), + ] + assert ( + helpers.call_action( + "dashboard_new_activities_count", context={"user": user["id"]} + ) + == 2 + ) + + def test_activities_on_a_followed_group(self): + user = factories.User() + another_user = factories.Sysadmin() + _clear_activities() + group = factories.Group(user=user) + helpers.call_action( + "follow_group", context={"user": user["name"]}, **group + ) + group["title"] = "Group with changed title" + helpers.call_action( + "group_update", context={"user": another_user["name"]}, **group + ) + + activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [ + (activity["activity_type"], activity["is_new"]) + for activity in activities[::-1] + ] == [ + ("new group", False), # False because user did this one herself + ("changed group", True), + ] + assert ( + helpers.call_action( + "dashboard_new_activities_count", context={"user": user["id"]} + ) + == 1 + ) + + def test_activities_on_a_dataset_in_a_followed_group(self): + user = factories.User() + another_user = factories.Sysadmin() + group = factories.Group(user=user) + helpers.call_action( + "follow_group", context={"user": user["name"]}, **group + ) + _clear_activities() + dataset = factories.Dataset( + groups=[{"name": group["name"]}], user=another_user + ) + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": another_user["name"]}, **dataset + ) + + activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [ + (activity["activity_type"], activity["is_new"]) + for activity in activities[::-1] + ] == [("new package", True), ("changed package", True)] + assert ( + helpers.call_action( + "dashboard_new_activities_count", context={"user": user["id"]} + ) + == 2 + ) + + def test_activities_on_a_dataset_in_a_followed_org(self): + user = factories.User() + another_user = factories.Sysadmin() + org = factories.Organization(user=user) + helpers.call_action( + "follow_group", context={"user": user["name"]}, **org + ) + _clear_activities() + dataset = factories.Dataset(owner_org=org["id"], user=another_user) + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": another_user["name"]}, **dataset + ) + + activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [ + (activity["activity_type"], activity["is_new"]) + for activity in activities[::-1] + ] == [("new package", True), ("changed package", True)] + assert ( + helpers.call_action( + "dashboard_new_activities_count", context={"user": user["id"]} + ) + == 2 + ) + + def test_activities_that_should_not_show(self): + user = factories.User() + _clear_activities() + # another_user does some activity unconnected with user + another_user = factories.Sysadmin() + group = factories.Group(user=another_user) + dataset = factories.Dataset( + groups=[{"name": group["name"]}], user=another_user + ) + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": another_user["name"]}, **dataset + ) + + activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [ + (activity["activity_type"], activity["is_new"]) + for activity in activities[::-1] + ] == [] + assert ( + helpers.call_action( + "dashboard_new_activities_count", context={"user": user["id"]} + ) + == 0 + ) + + @pytest.mark.ckan_config("ckan.activity_list_limit", "5") + def test_maximum_number_of_new_activities(self): + """Test that the new activities count does not go higher than 5, even + if there are more than 5 new activities from the user's followers.""" + user = factories.User() + another_user = factories.Sysadmin() + dataset = factories.Dataset() + helpers.call_action( + "follow_dataset", context={"user": user["name"]}, **dataset + ) + for n in range(0, 7): + dataset["notes"] = "Updated {n} times".format(n=n) + helpers.call_action( + "package_update", + context={"user": another_user["name"]}, + **dataset, + ) + assert ( + helpers.call_action( + "dashboard_new_activities_count", context={"user": user["id"]} + ) + == 5 + ) + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("clean_db", "with_request_context", "with_plugins") +class TestSendEmailNotifications(object): + # TODO: this action doesn't do much. Maybe it well be better to move tests + # into lib.email_notifications eventually + + def check_email(self, email, address, name, subject): + assert email[1] == "info@test.ckan.net" + assert email[2] == [address] + assert subject in email[3] + # TODO: Check that body contains link to dashboard and email prefs. + + def test_fresh_setupnotifications(self, mail_server): + helpers.call_action("send_email_notifications") + assert ( + len(mail_server.get_smtp_messages()) == 0 + ), "Notification came out of nowhere" + + def test_single_notification(self, mail_server): + pkg = factories.Dataset() + user = factories.User(activity_streams_email_notifications=True) + helpers.call_action( + "follow_dataset", {"user": user["name"]}, id=pkg["id"] + ) + helpers.call_action("package_update", id=pkg["id"], notes="updated") + helpers.call_action("send_email_notifications") + messages = mail_server.get_smtp_messages() + assert len(messages) == 1 + self.check_email( + messages[0], + user["email"], + user["name"], + "1 new activity from CKAN", + ) + + def test_multiple_notifications(self, mail_server): + pkg = factories.Dataset() + user = factories.User(activity_streams_email_notifications=True) + helpers.call_action( + "follow_dataset", {"user": user["name"]}, id=pkg["id"] + ) + for i in range(3): + helpers.call_action( + "package_update", id=pkg["id"], notes=f"updated {i} times" + ) + helpers.call_action("send_email_notifications") + messages = mail_server.get_smtp_messages() + assert len(messages) == 1 + self.check_email( + messages[0], + user["email"], + user["name"], + "3 new activities from CKAN", + ) + + def test_no_notifications_if_dashboard_visited(self, mail_server): + pkg = factories.Dataset() + user = factories.User(activity_streams_email_notifications=True) + helpers.call_action( + "follow_dataset", {"user": user["name"]}, id=pkg["id"] + ) + helpers.call_action("package_update", id=pkg["id"], notes="updated") + new_activities_count = helpers.call_action( + "dashboard_new_activities_count", + {"user": user["name"]}, + id=pkg["id"], + ) + assert new_activities_count == 1 + + helpers.call_action( + "dashboard_mark_activities_old", + {"user": user["name"]}, + id=pkg["id"], + ) + helpers.call_action("send_email_notifications") + messages = mail_server.get_smtp_messages() + assert len(messages) == 0 + + def test_notifications_disabled_by_default(self): + user = factories.User() + assert not user["activity_streams_email_notifications"] + + def test_no_emails_when_notifications_disabled(self, mail_server): + pkg = factories.Dataset() + user = factories.User() + helpers.call_action( + "follow_dataset", {"user": user["name"]}, id=pkg["id"] + ) + helpers.call_action("package_update", id=pkg["id"], notes="updated") + helpers.call_action("send_email_notifications") + messages = mail_server.get_smtp_messages() + assert len(messages) == 0 + new_activities_count = helpers.call_action( + "dashboard_new_activities_count", + {"user": user["name"]}, + id=pkg["id"], + ) + assert new_activities_count == 1 + + @pytest.mark.ckan_config( + "ckan.activity_streams_email_notifications", False + ) + def test_send_email_notifications_feature_disabled(self, mail_server): + with pytest.raises(tk.ValidationError): + helpers.call_action("send_email_notifications") + messages = mail_server.get_smtp_messages() + assert len(messages) == 0 + + @pytest.mark.ckan_config("ckan.email_notifications_since", ".000001") + def test_email_notifications_since(self, mail_server): + pkg = factories.Dataset() + user = factories.User(activity_streams_email_notifications=True) + helpers.call_action( + "follow_dataset", {"user": user["name"]}, id=pkg["id"] + ) + helpers.call_action("package_update", id=pkg["id"], notes="updated") + time.sleep(0.01) + helpers.call_action("send_email_notifications") + messages = mail_server.get_smtp_messages() + assert len(messages) == 0 + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("non_clean_db", "with_plugins") +class TestDashboardMarkActivitiesOld(object): + def test_mark_as_old_some_activities_by_a_followed_user(self): + # do some activity that will show up on user's dashboard + user = factories.User() + # now some activity that is "new" because it is by a followed user + followed_user = factories.User() + helpers.call_action( + "follow_user", context={"user": user["name"]}, **followed_user + ) + dataset = factories.Dataset(user=followed_user) + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", + context={"user": followed_user["name"]}, + **dataset, + ) + assert ( + helpers.call_action( + "dashboard_new_activities_count", context={"user": user["id"]} + ) + == 3 + ) + activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [ + (activity["activity_type"], activity["is_new"]) + for activity in activities[::-1] + ] == [ + ("new user", False), + ("new user", True), + ("new package", True), + ("changed package", True), + ] + + helpers.call_action( + "dashboard_mark_activities_old", context={"user": user["name"]} + ) + + assert ( + helpers.call_action( + "dashboard_new_activities_count", context={"user": user["id"]} + ) + == 0 + ) + activities = helpers.call_action( + "dashboard_activity_list", context={"user": user["id"]} + ) + assert [ + (activity["activity_type"], activity["is_new"]) + for activity in activities[::-1] + ] == [ + ("new user", False), + ("new user", False), + ("new package", False), + ("changed package", False), + ] + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("non_clean_db", "with_plugins") +class TestFollow: + @pytest.mark.usefixtures("app") + def test_follow_dataset_no_activity(self): + user = factories.User() + dataset = factories.Dataset() + _clear_activities() + helpers.call_action( + "follow_dataset", context={"user": user["name"]}, id=dataset["id"] + ) + assert not helpers.call_action("user_activity_list", id=user["id"]) + + @pytest.mark.usefixtures("app") + def test_follow_group_no_activity(self): + user = factories.User() + group = factories.Group() + _clear_activities() + helpers.call_action( + "follow_group", context={"user": user["name"]}, **group + ) + assert not helpers.call_action("user_activity_list", id=user["id"]) + + @pytest.mark.usefixtures("app") + def test_follow_organization_no_activity(self): + user = factories.User() + org = factories.Organization() + _clear_activities() + helpers.call_action( + "follow_group", context={"user": user["name"]}, **org + ) + assert not helpers.call_action("user_activity_list", id=user["id"]) + + @pytest.mark.usefixtures("app") + def test_follow_user_no_activity(self): + user = factories.User() + user2 = factories.User() + _clear_activities() + helpers.call_action( + "follow_user", context={"user": user["name"]}, **user2 + ) + assert not helpers.call_action("user_activity_list", id=user["id"]) + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("clean_db", "with_plugins") +class TestDeferCommitOnCreate(object): + + def test_package_create_defer_commit(self): + dataset_dict = { + "name": "test_dataset", + } + context = { + "defer_commit": True, + "user": factories.User()["name"], + } + + helpers.call_action("package_create", context=context, **dataset_dict) + + model.Session.close() + + with pytest.raises(tk.ObjectNotFound): + helpers.call_action("package_show", id=dataset_dict["name"]) + + assert model.Session.query(Activity).filter( + Activity.activity_type != "new user").count() == 0 + + def test_group_create_defer_commit(self): + group_dict = { + "name": "test_group", + } + context = { + "defer_commit": True, + "user": factories.User()["name"], + } + + helpers.call_action("group_create", context=context, **group_dict) + + model.Session.close() + + with pytest.raises(tk.ObjectNotFound): + helpers.call_action("group_show", id=group_dict["name"]) + + assert model.Session.query(Activity).filter( + Activity.activity_type != "new user").count() == 0 + + def test_organization_create_defer_commit(self): + organization_dict = { + "name": "test_org", + } + context = { + "defer_commit": True, + "user": factories.User()["name"], + } + + helpers.call_action("organization_create", context=context, **organization_dict) + + model.Session.close() + + with pytest.raises(tk.ObjectNotFound): + helpers.call_action("organization_show", id=organization_dict["name"]) + + assert model.Session.query(Activity).filter( + Activity.activity_type != "new user").count() == 0 + + def test_user_create_defer_commit(self): + stub = factories.User.stub() + user_dict = { + "name": stub.name, + "email": stub.email, + "password": "test1234", + } + context = {"defer_commit": True} + + helpers.call_action("user_create", context=context, **user_dict) + + model.Session.close() + + with pytest.raises(tk.ObjectNotFound): + helpers.call_action("user_show", id=user_dict["name"]) + + assert model.Session.query(Activity).count() == 0 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_auth.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_auth.py new file mode 100644 index 0000000..eb11bf6 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_auth.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +import pytest + +import ckan.plugins.toolkit as tk +import ckan.tests.helpers as helpers +import ckan.model as model + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("with_plugins") +class TestAuth: + @pytest.mark.ckan_config( + "ckan.auth.public_activity_stream_detail", "false" + ) + def test_config_option_public_activity_stream_detail_denied(self, package): + """Config option says an anon user is not authorized to get activity + stream data/detail. + """ + context = {"user": None, "model": model} + with pytest.raises(tk.NotAuthorized): + helpers.call_auth( + "package_activity_list", + context=context, + id=package["id"], + include_data=True, + ) + + @pytest.mark.ckan_config("ckan.auth.public_activity_stream_detail", "true") + def test_config_option_public_activity_stream_detail(self, package): + """Config option says an anon user is authorized to get activity + stream data/detail. + """ + context = {"user": None, "model": model} + helpers.call_auth( + "package_activity_list", + context=context, + id=package["id"], + include_data=True, + ) + + def test_normal_user_cant_use_it(self, user): + context = {"user": user["name"], "model": model} + + with pytest.raises(tk.NotAuthorized): + helpers.call_auth("activity_create", context=context) diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_functional.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_functional.py new file mode 100644 index 0000000..23f3828 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/tests/logic/test_functional.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +import pytest + +import ckan.plugins.toolkit as tk +import ckan.tests.helpers as helpers +import ckan.tests.factories as factories + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("clean_db", "with_plugins", "reset_index") +@pytest.mark.ckan_config("ckan.activity_list_limit", "5") +@pytest.mark.ckan_config("ckan.activity_list_limit_max", "7") +class TestPagination(): + + def test_pagination(self, app): + user = factories.User() + org = factories.Organization() + dataset = factories.Dataset(owner_org=org["id"]) + + for i in range(0, 8): + dataset["notes"] = f"Update number: {i}" + helpers.call_action( + "package_update", + context={"user": user["name"]}, + **dataset, + ) + + # Test initial pagination buttons are rendered correctly + url = tk.url_for("dataset.activity", id=dataset["id"]) + response = app.get(url) + + assert 'Newer activities' in response.body + assert f'Newer activities' in response.body + assert f'February 1, 2018 at 10:58:59 AM UTC' + "" + ) + assert hasattr(html, "__html__") # shows it is safe Markup + + def test_selected(self): + pkg_activity = { + "id": "id1", + "timestamp": datetime.datetime(2018, 2, 1, 10, 58, 59), + } + + out = tk.h.activity_list_select([pkg_activity], "id1") + + html = out[0] + assert ( + str(html) + == '" + ) + assert hasattr(html, "__html__") # shows it is safe Markup + + def test_escaping(self): + pkg_activity = { + "id": '">', # hacked somehow + "timestamp": datetime.datetime(2018, 2, 1, 10, 58, 59), + } + + out = tk.h.activity_list_select([pkg_activity], "") + + html = out[0] + assert str(html).startswith('{}'.format(user["name"], user["fullname"]) + in response + ) + + +def assert_group_link_in_response(group, response): + assert ( + '{2}'.format(group["type"], group["name"], group["title"]) + in response + ) + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("with_plugins", "clean_db") +class TestOrganization(object): + def test_simple(self, app): + """Checking the template shows the activity stream.""" + user = factories.User() + org = factories.Organization(user=user) + + url = url_for("activity.organization_activity", id=org["id"]) + response = app.get(url) + assert user["fullname"] in response + assert "created the organization" in response + + def test_create_organization(self, app): + user = factories.User() + org = factories.Organization(user=user) + + url = url_for("activity.organization_activity", id=org["id"]) + response = app.get(url) + assert_user_link_in_response(user, response) + assert "created the organization" in response + assert_group_link_in_response(org, response) + + def test_change_organization(self, app): + user = factories.User() + org = factories.Organization(user=user) + _clear_activities() + org["title"] = "Organization with changed title" + helpers.call_action( + "organization_update", context={"user": user["name"]}, **org + ) + + url = url_for("activity.organization_activity", id=org["id"]) + response = app.get(url) + assert_user_link_in_response(user, response) + assert "updated the organization" in response + assert_group_link_in_response(org, response) + + def test_delete_org_using_organization_delete(self, app): + user = factories.User() + org = factories.Organization(user=user) + _clear_activities() + helpers.call_action( + "organization_delete", context={"user": user["name"]}, **org + ) + + url = url_for("activity.organization_activity", id=org["id"]) + env = {"REMOTE_USER": user["name"]} + app.get(url, extra_environ=env, status=404) + # organization_delete causes the Member to state=deleted and then the + # user doesn't have permission to see their own deleted Organization. + # Therefore you can't render the activity stream of that org. You'd + # hope that organization_delete was the same as organization_update + # state=deleted but they are not... + + def test_delete_org_by_updating_state(self, app): + user = factories.User() + org = factories.Organization(user=user) + _clear_activities() + org["state"] = "deleted" + helpers.call_action( + "organization_update", context={"user": user["name"]}, **org + ) + + url = url_for("activity.organization_activity", id=org["id"]) + env = {"REMOTE_USER": user["name"]} + response = app.get(url, extra_environ=env) + assert_user_link_in_response(user, response) + assert "deleted the organization" in response + assert_group_link_in_response(org, response) + + def test_create_dataset(self, app): + user = factories.User() + org = factories.Organization() + _clear_activities() + dataset = factories.Dataset(owner_org=org["id"], user=user) + + url = url_for("activity.organization_activity", id=org["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "created the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + def test_change_dataset(self, app): + user = factories.User() + org = factories.Organization() + dataset = factories.Dataset(owner_org=org["id"], user=user) + _clear_activities() + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + url = url_for("activity.organization_activity", id=org["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "updated the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + def test_delete_dataset(self, app): + user = factories.User() + org = factories.Organization() + dataset = factories.Dataset(owner_org=org["id"], user=user) + _clear_activities() + helpers.call_action( + "package_delete", context={"user": user["name"]}, **dataset + ) + + url = url_for("activity.organization_activity", id=org["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "deleted the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("non_clean_db", "with_plugins") +class TestUser: + def test_simple(self, app): + """Checking the template shows the activity stream.""" + + user = factories.User() + + url = url_for("activity.user_activity", id=user["id"]) + response = app.get(url) + assert user["fullname"] in response + assert "signed up" in response + + def test_create_user(self, app): + + user = factories.User() + + url = url_for("activity.user_activity", id=user["id"]) + response = app.get(url) + assert_user_link_in_response(user, response) + assert "signed up" in response + + def test_change_user(self, app): + + user = factories.User() + _clear_activities() + user["fullname"] = "Mr. Changed Name" + helpers.call_action( + "user_update", context={"user": user["name"]}, **user + ) + + url = url_for("activity.user_activity", id=user["id"]) + response = app.get(url) + assert_user_link_in_response(user, response) + assert "updated their profile" in response + + def test_create_dataset(self, app): + + user = factories.User() + _clear_activities() + dataset = factories.Dataset(user=user) + + url = url_for("activity.user_activity", id=user["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "created the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + def test_change_dataset(self, app): + + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + url = url_for("activity.user_activity", id=user["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + + assert_user_link_in_response(user, response) + assert "updated the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + def test_delete_dataset(self, app): + + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + helpers.call_action( + "package_delete", context={"user": user["name"]}, **dataset + ) + + url = url_for("activity.user_activity", id=user["id"]) + env = {"REMOTE_USER": user["name"]} + response = app.get(url, extra_environ=env) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "deleted the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + def test_create_group(self, app): + + user = factories.User() + group = factories.Group(user=user) + + url = url_for("activity.user_activity", id=user["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".group") + + assert_user_link_in_response(user, response) + assert "created the group" in response + assert group["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert group["title"] in href.text.strip() + + def test_change_group(self, app): + + user = factories.User() + group = factories.Group(user=user) + _clear_activities() + group["title"] = "Group with changed title" + helpers.call_action( + "group_update", context={"user": user["name"]}, **group + ) + + url = url_for("activity.user_activity", id=user["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".group") + assert_user_link_in_response(user, response) + assert "updated the group" in response + assert group["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert group["title"] in href.text.strip() + + def test_delete_group_using_group_delete(self, app): + + user = factories.User() + group = factories.Group(user=user) + _clear_activities() + helpers.call_action( + "group_delete", context={"user": user["name"]}, **group + ) + + url = url_for("activity.user_activity", id=user["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".group") + assert_user_link_in_response(user, response) + assert "deleted the group" in response + assert group["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert group["title"] in href.text.strip() + + def test_delete_group_by_updating_state(self, app): + + user = factories.User() + group = factories.Group(user=user) + _clear_activities() + group["state"] = "deleted" + helpers.call_action( + "group_update", context={"user": user["name"]}, **group + ) + + url = url_for("activity.group_activity", id=group["id"]) + env = {"REMOTE_USER": user["name"]} + response = app.get(url, extra_environ=env) + assert_user_link_in_response(user, response) + assert "deleted the group" in response + assert_group_link_in_response(group, response) + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("clean_db", "with_plugins") +class TestPackage: + def test_simple(self, app): + """Checking the template shows the activity stream.""" + user = factories.User() + dataset = factories.Dataset(user=user) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url) + assert user["fullname"] in response + assert "created the dataset" in response + + def test_create_dataset(self, app): + + user = factories.User() + dataset = factories.Dataset(user=user) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + + assert_user_link_in_response(user, response) + assert "created the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + def test_change_dataset(self, app): + + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "updated the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + def test_create_tag_directly(self, app): + + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + dataset["tags"] = [{"name": "some_tag"}] + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "updated the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + + assert len(activities) == 1 + + def test_create_tag(self, app): + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + dataset["tags"] = [{"name": "some_tag"}] + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "updated the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + + assert len(activities) == 1 + + def test_create_extra(self, app): + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + dataset["extras"] = [{"key": "some", "value": "extra"}] + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "updated the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + + assert len(activities) == 1 + + def test_create_resource(self, app): + user = factories.User() + dataset = factories.Dataset(user=user) + _clear_activities() + helpers.call_action( + "resource_create", + context={"user": user["name"]}, + name="Test resource", + package_id=dataset["id"], + ) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + + assert_user_link_in_response(user, response) + assert "updated the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + + assert len(activities) == 1 + + def test_update_resource(self, app): + user = factories.User() + dataset = factories.Dataset(user=user) + resource = factories.Resource(package_id=dataset["id"]) + _clear_activities() + + helpers.call_action( + "resource_update", + context={"user": user["name"]}, + id=resource["id"], + name="Test resource updated", + package_id=dataset["id"], + ) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "updated the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + + assert len(activities) == 1 + + def test_delete_dataset(self, app): + user = factories.User() + org = factories.Organization() + dataset = factories.Dataset(owner_org=org["id"], user=user) + _clear_activities() + helpers.call_action( + "package_delete", context={"user": user["name"]}, **dataset + ) + + url = url_for("activity.organization_activity", id=org["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + + assert_user_link_in_response(user, response) + assert "deleted the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + def test_admin_can_see_old_versions(self, app): + + user = factories.User() + env = {"REMOTE_USER": user["name"]} + dataset = factories.Dataset(user=user) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url, extra_environ=env) + assert "View this version" in response + + def test_public_cant_see_old_versions(self, app): + + user = factories.User() + dataset = factories.Dataset(user=user) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url) + assert "View this version" not in response + + def test_admin_can_see_changes(self, app): + + user = factories.User() + env = {"REMOTE_USER": user["name"]} + dataset = factories.Dataset() # activities by system user aren't shown + dataset["title"] = "Changed" + helpers.call_action("package_update", **dataset) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url, extra_environ=env) + assert "Changes" in response + + def test_public_cant_see_changes(self, app): + dataset = factories.Dataset() # activities by system user aren't shown + dataset["title"] = "Changed" + helpers.call_action("package_update", **dataset) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url) + assert "Changes" not in response + + # ckanext-canada uses their IActivity to add their custom activity to the + # list of validators: https://github.com/open-data/ckanext-canada/blob/6870e5bc38a04aa8cef191b5e9eb361f9560872b/ckanext/canada/plugins.py#L596 + # but it's easier here to just hack patch it in + @mock.patch( + "ckanext.activity.logic.validators.object_id_validators", + dict( + list(object_id_validators.items()) + + [("changed datastore", "package_id_exists")] + ), + ) + def test_custom_activity(self, app): + """Render a custom activity""" + + user = factories.User() + organization = factories.Organization( + users=[{"name": user["id"], "capacity": "admin"}] + ) + dataset = factories.Dataset(owner_org=organization["id"], user=user) + resource = factories.Resource(package_id=dataset["id"]) + _clear_activities() + + # Create a custom Activity object. This one is inspired by: + # https://github.com/open-data/ckanext-canada/blob/master/ckanext/canada/activity.py + activity_dict = { + "user_id": user["id"], + "object_id": dataset["id"], + "activity_type": "changed datastore", + "data": { + "resource_id": resource["id"], + "pkg_type": dataset["type"], + "resource_name": "june-2018", + "owner_org": organization["name"], + "count": 5, + }, + } + helpers.call_action("activity_create", **activity_dict) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url) + assert_user_link_in_response(user, response) + # it renders the activity with fallback.html, since we've not defined + # changed_datastore.html in this case + assert "changed datastore" in response + + def test_redirect_also_with_activity_parameter(self, app): + user = factories.User() + dataset = factories.Dataset(user=user) + activity = activity_model.package_activity_list( + dataset["id"], limit=1, offset=0 + )[0] + # view as an admin because viewing the old versions of a dataset + sysadmin = factories.Sysadmin() + env = {"REMOTE_USER": sysadmin["name"]} + response = app.get( + url_for( + "activity.package_history", + id=dataset["id"], + activity_id=activity.id, + ), + status=302, + extra_environ=env, + follow_redirects=False, + ) + expected_path = url_for( + "activity.package_history", + id=dataset["name"], + _external=True, + activity_id=activity.id, + ) + assert response.headers["location"] == expected_path + + def test_read_dataset_as_it_used_to_be(self, app): + dataset = factories.Dataset(title="Original title") + activity = ( + model.Session.query(Activity) + .filter_by(object_id=dataset["id"]) + .one() + ) + dataset["title"] = "Changed title" + helpers.call_action("package_update", **dataset) + + sysadmin = factories.Sysadmin() + env = {"REMOTE_USER": sysadmin["name"]} + response = app.get( + url_for( + "activity.package_history", + id=dataset["name"], + activity_id=activity.id, + ), + extra_environ=env, + ) + assert helpers.body_contains(response, "Original title") + + def test_read_dataset_as_it_used_to_be_but_is_unmigrated(self, app): + # Renders the dataset using the activity detail, when that Activity was + # created with an earlier version of CKAN, and it has not been migrated + # (with migrate_package_activity.py), which should give a 404 + + user = factories.User() + dataset = factories.Dataset(user=user) + + # delete the modern Activity object that's been automatically created + modern_activity = ( + model.Session.query(Activity) + .filter_by(object_id=dataset["id"]) + .one() + ) + modern_activity.delete() + + # Create an Activity object as it was in earlier versions of CKAN. + # This code is based on: + # https://github.com/ckan/ckan/blob/b348bf2fe68db6704ea0a3e22d533ded3d8d4344/ckan/model/package.py#L508 + activity_type = "changed" + dataset_table_dict = dictization.table_dictize( + model.Package.get(dataset["id"]), context={"model": model} + ) + activity = Activity( + user_id=user["id"], + object_id=dataset["id"], + activity_type="%s package" % activity_type, + data={ + # "actor": a legacy activity had no "actor" + # "package": a legacy activity had just the package table, + # rather than the result of package_show + "package": dataset_table_dict + }, + ) + model.Session.add(activity) + + sysadmin = factories.Sysadmin() + env = {"REMOTE_USER": sysadmin["name"]} + app.get( + url_for( + "activity.package_history", + id=dataset["name"], + activity_id=activity.id, + ), + extra_environ=env, + status=404, + ) + + def test_changes(self, app): + user = factories.User() + dataset = factories.Dataset(title="First title", user=user) + dataset["title"] = "Second title" + helpers.call_action("package_update", **dataset) + + activity = activity_model.package_activity_list( + dataset["id"], limit=1, offset=0 + )[0] + env = {"REMOTE_USER": user["name"]} + response = app.get( + url_for("activity.package_changes", id=activity.id), + extra_environ=env, + ) + assert helpers.body_contains(response, "First") + assert helpers.body_contains(response, "Second") + + @pytest.mark.ckan_config("ckan.activity_list_limit", "3") + def test_invalid_get_params(self, app): + + user = factories.User() + dataset = factories.Dataset(user=user) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url, query_string={"before": "XXX"}, status=400) + assert "Invalid parameters" in response.body + + @pytest.mark.ckan_config("ckan.activity_list_limit", "3") + def test_older_activities_url_button(self, app): + + user = factories.User() + dataset = factories.Dataset(user=user) + + dataset["title"] = "Second title" + helpers.call_action("package_update", **dataset) + dataset["title"] = "Third title" + helpers.call_action("package_update", **dataset) + dataset["title"] = "Fourth title" + helpers.call_action("package_update", **dataset) + + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get(url) + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + # Last activity in the first page + before_time = datetime.fromisoformat(activities[2]["timestamp"]) + + # Next page button + older_activities_url_url = "/dataset/activity/{}?before={}".format( + dataset["id"], before_time.timestamp() + ) + assert older_activities_url_url in response.body + + # Prev page button is not in the first page + newer_activities_url_url = "/dataset/activity/{}?after=".format(dataset["id"]) + assert newer_activities_url_url not in response.body + + @pytest.mark.ckan_config("ckan.activity_list_limit", "3") + def test_next_before_buttons(self, app): + + user = factories.User() + dataset = factories.Dataset(user=user) + + dataset["title"] = "Second title" + helpers.call_action("package_update", **dataset) + dataset["title"] = "Third title" + helpers.call_action("package_update", **dataset) + dataset["title"] = "4th title" + helpers.call_action("package_update", **dataset) + dataset["title"] = "5th title" + helpers.call_action("package_update", **dataset) + dataset["title"] = "6th title" + helpers.call_action("package_update", **dataset) + dataset["title"] = "7h title" + helpers.call_action("package_update", **dataset) + + db_activities = activity_model.package_activity_list( + dataset["id"], limit=10 + ) + activities = helpers.call_action( + "package_activity_list", id=dataset["id"] + ) + # Last activity in the first page + last_act_page_1_time = datetime.fromisoformat( + activities[2]["timestamp"] + ) + url = url_for("activity.package_activity", id=dataset["id"]) + response = app.get( + url, query_string={"before": last_act_page_1_time.timestamp()} + ) + + # Next page button exists in page 2 + older_activities_url_url = "/dataset/activity/{}?before={}".format( + dataset["id"], db_activities[5].timestamp.timestamp() + ) + assert older_activities_url_url in response.body + # Prev page button exists in page 2 + newer_activities_url_url = "/dataset/activity/{}?after={}".format( + dataset["id"], db_activities[3].timestamp.timestamp() + ) + assert newer_activities_url_url in response.body + + @pytest.mark.ckan_config("ckan.activity_list_limit", "3") + def test_newer_activities_url_button(self, app): + + user = factories.User() + dataset = factories.Dataset(user=user) + + dataset["title"] = "Second title" + helpers.call_action("package_update", **dataset) + dataset["title"] = "Third title" + helpers.call_action("package_update", **dataset) + dataset["title"] = "Fourth title" + helpers.call_action("package_update", **dataset) + + activities = helpers.call_action( + "package_activity_list", id=dataset["id"], limit=10 + ) + before_time = datetime.fromisoformat(activities[2]["timestamp"]) + + url = url_for("activity.package_activity", id=dataset["id"]) + # url for page 2 + response = app.get( + url, query_string={"before": before_time.timestamp()} + ) + + # There's not a third page + older_activities_url_url = "/dataset/activity/{}?before=".format(dataset["name"]) + assert older_activities_url_url not in response.body + + # previous page exists + after_time = datetime.fromisoformat(activities[3]["timestamp"]) + newer_activities_url_url = "/dataset/activity/{}?after={}".format( + dataset["id"], after_time.timestamp() + ) + assert newer_activities_url_url in response.body + + +@pytest.mark.ckan_config("ckan.plugins", "activity") +@pytest.mark.usefixtures("non_clean_db", "with_plugins") +class TestGroup: + def test_simple(self, app): + """Checking the template shows the activity stream.""" + user = factories.User() + group = factories.Group(user=user) + + url = url_for("activity.group_activity", id=group["id"]) + response = app.get(url) + assert user["fullname"] in response + assert "created the group" in response + + def test_create_group(self, app): + user = factories.User() + group = factories.Group(user=user) + + url = url_for("activity.group_activity", id=group["id"]) + response = app.get(url) + assert_user_link_in_response(user, response) + assert "created the group" in response + assert_group_link_in_response(group, response) + + def test_change_group(self, app): + user = factories.User() + group = factories.Group(user=user) + _clear_activities() + group["title"] = "Group with changed title" + helpers.call_action( + "group_update", context={"user": user["name"]}, **group + ) + + url = url_for("activity.group_activity", id=group["id"]) + response = app.get(url) + assert_user_link_in_response(user, response) + assert "updated the group" in response + assert_group_link_in_response(group, response) + + def test_delete_group_using_group_delete(self, app): + user = factories.User() + group = factories.Group(user=user) + _clear_activities() + helpers.call_action( + "group_delete", context={"user": user["name"]}, **group + ) + + url = url_for("activity.group_activity", id=group["id"]) + env = {"REMOTE_USER": user["name"]} + app.get(url, extra_environ=env, status=404) + # group_delete causes the Member to state=deleted and then the user + # doesn't have permission to see their own deleted Group. Therefore you + # can't render the activity stream of that group. You'd hope that + # group_delete was the same as group_update state=deleted but they are + # not... + + def test_delete_group_by_updating_state(self, app): + user = factories.User() + group = factories.Group(user=user) + _clear_activities() + group["state"] = "deleted" + helpers.call_action( + "group_update", context={"user": user["name"]}, **group + ) + + url = url_for("activity.group_activity", id=group["id"]) + env = {"REMOTE_USER": user["name"]} + response = app.get(url, extra_environ=env) + assert_user_link_in_response(user, response) + assert "deleted the group" in response + assert_group_link_in_response(group, response) + + def test_create_dataset(self, app): + user = factories.User() + group = factories.Group(user=user) + _clear_activities() + dataset = factories.Dataset(groups=[{"id": group["id"]}], user=user) + + url = url_for("activity.group_activity", id=group["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "created the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + def test_change_dataset(self, app): + + user = factories.User() + group = factories.Group(user=user) + dataset = factories.Dataset(groups=[{"id": group["id"]}], user=user) + _clear_activities() + dataset["title"] = "Dataset with changed title" + helpers.call_action( + "package_update", context={"user": user["name"]}, **dataset + ) + + url = url_for("activity.group_activity", id=group["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "updated the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() + + def test_delete_dataset(self, app): + user = factories.User() + group = factories.Group(user=user) + dataset = factories.Dataset(groups=[{"id": group["id"]}], user=user) + _clear_activities() + helpers.call_action( + "package_delete", context={"user": user["name"]}, **dataset + ) + + url = url_for("activity.group_activity", id=group["id"]) + response = app.get(url) + page = BeautifulSoup(response.body) + href = page.select_one(".dataset") + assert_user_link_in_response(user, response) + assert "deleted the dataset" in response + assert dataset["id"] in href.select_one("a")["href"].split("/", 2)[-1] + assert dataset["title"] in href.text.strip() diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/views.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/views.py new file mode 100644 index 0000000..8374d88 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/activity/views.py @@ -0,0 +1,943 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations +import logging + +from datetime import datetime +from typing import Any, Optional, Union, cast + +from flask import Blueprint + +import ckan.plugins.toolkit as tk +import ckan.model as model +from ckan.views.group import ( + set_org, + # TODO: don't use hidden funcitons + _get_group_dict, + _get_group_template, + _replace_group_org, +) + +# TODO: don't use hidden funcitons +from ckan.views.user import _extra_template_variables + +# TODO: don't use hidden funcitons +from ckan.views.dataset import _setup_template_variables + +from ckan.types import Context, Response +from .model import Activity +from .logic.validators import ( + VALIDATORS_PACKAGE_ACTIVITY_TYPES, + VALIDATORS_GROUP_ACTIVITY_TYPES, + VALIDATORS_ORGANIZATION_ACTIVITY_TYPES +) + + +log = logging.getLogger(__name__) +bp = Blueprint("activity", __name__) + + +def _get_activity_stream_limit() -> int: + base_limit = tk.config.get("ckan.activity_list_limit") + max_limit = tk.config.get("ckan.activity_list_limit_max") + return min(base_limit, max_limit) + + +def _get_older_activities_url( + has_more: bool, + stream: list[dict[str, Any]], + **kwargs: Any +) -> Any: + """ Returns pagination's older activities url. + + If "after", we came from older activities, so we know it exists. + if "before" (or is_first_page), we only show older activities if we know + we have more rows + """ + after = tk.request.args.get("after") + before = tk.request.args.get("before") + is_first_page = after is None and before is None + url = None + if after or (has_more and (before or is_first_page)): + before_time = datetime.fromisoformat( + stream[-1]["timestamp"] + ) + url = tk.h.url_for( + tk.request.endpoint, + before=before_time.timestamp(), + **kwargs + ) + + return url + + +def _get_newer_activities_url( + has_more: bool, + stream: list[dict[str, Any]], + **kwargs: Any +) -> Any: + """ Returns pagination's newer activities url. + + if "before", we came from the newer activities, so it exists. + if "after", we only show newer activities if we know + we have more rows + """ + after = tk.request.args.get("after") + before = tk.request.args.get("before") + url = None + + if before or (has_more and after): + after_time = datetime.fromisoformat( + stream[0]["timestamp"] + ) + url = tk.h.url_for( + tk.request.endpoint, + after=after_time.timestamp(), + **kwargs + ) + return url + + +@bp.route("/dataset//resources//history/") +def resource_history(id: str, resource_id: str, activity_id: str) -> str: + context = cast( + Context, + { + "auth_user_obj": tk.g.userobj, + "for_view": True, + }, + ) + + try: + package = tk.get_action("package_show")(context, {"id": id}) + except (tk.ObjectNotFound, tk.NotAuthorized): + return tk.abort(404, tk._("Dataset not found")) + + # view an 'old' version of the package, as recorded in the + # activity stream + current_pkg = package + try: + activity = context["session"].query(Activity).get(activity_id) + assert activity + package = activity.data["package"] + except AttributeError: + tk.abort(404, tk._("Dataset not found")) + + if package["id"] != current_pkg["id"]: + log.info( + "Mismatch between pkg id in activity and URL {} {}".format( + package["id"], current_pkg["id"] + ) + ) + # the activity is not for the package in the URL - don't allow + # misleading URLs as could be malicious + tk.abort(404, tk._("Activity not found")) + # The name is used lots in the template for links, so fix it to be + # the current one. It's not displayed to the user anyway. + package["name"] = current_pkg["name"] + + # Don't crash on old (unmigrated) activity records, which do not + # include resources or extras. + package.setdefault("resources", []) + + resource = None + for res in package.get("resources", []): + if res["id"] == resource_id: + resource = res + break + if not resource: + return tk.abort(404, tk._("Resource not found")) + + # get package license info + license_id = package.get("license_id") + try: + package["isopen"] = model.Package.get_license_register()[ + license_id + ].isopen() + except KeyError: + package["isopen"] = False + + resource_views = tk.get_action("resource_view_list")( + context, {"id": resource_id} + ) + resource["has_views"] = len(resource_views) > 0 + + current_resource_view = None + view_id = tk.request.args.get("view_id") + if resource["has_views"]: + if view_id: + current_resource_view = [ + rv for rv in resource_views if rv["id"] == view_id + ] + if len(current_resource_view) == 1: + current_resource_view = current_resource_view[0] + else: + return tk.abort(404, tk._("Resource view not found")) + else: + current_resource_view = resource_views[0] + + # required for nav menu + pkg = context["package"] + dataset_type = pkg.type + + # TODO: remove + tk.g.package = package + tk.g.resource = resource + tk.g.pkg = pkg + tk.g.pkg_dict = package + + extra_vars: dict[str, Any] = { + "resource_views": resource_views, + "current_resource_view": current_resource_view, + "dataset_type": dataset_type, + "pkg_dict": package, + "package": package, + "resource": resource, + "pkg": pkg, # NB it is the current version of the dataset, so ignores + # activity_id. Still used though in resource views for + # backward compatibility + } + + return tk.render("package/resource_history.html", extra_vars) + + +@bp.route("/dataset//history/") +def package_history(id: str, activity_id: str) -> Union[Response, str]: + context = cast( + Context, + { + "for_view": True, + "auth_user_obj": tk.g.userobj, + }, + ) + data_dict = {"id": id, "include_tracking": True} + + # check if package exists + try: + pkg_dict = tk.get_action("package_show")(context, data_dict) + pkg = context["package"] + except (tk.ObjectNotFound, tk.NotAuthorized): + return tk.abort(404, tk._("Dataset not found")) + + # if the user specified a package id, redirect to the package name + if ( + data_dict["id"] == pkg_dict["id"] + and data_dict["id"] != pkg_dict["name"] + ): + return tk.h.redirect_to( + "activity.package_history", + id=pkg_dict["name"], + activity_id=activity_id, + ) + + tk.g.pkg_dict = pkg_dict + tk.g.pkg = pkg + # NB templates should not use g.pkg, because it takes no account of + # activity_id + + # view an 'old' version of the package, as recorded in the + # activity stream + try: + activity = tk.get_action("activity_show")( + context, {"id": activity_id, "include_data": True} + ) + except tk.ObjectNotFound: + tk.abort(404, tk._("Activity not found")) + except tk.NotAuthorized: + tk.abort(403, tk._("Unauthorized to view activity data")) + current_pkg = pkg_dict + try: + pkg_dict = activity["data"]["package"] + except KeyError: + tk.abort(404, tk._("Dataset not found")) + if "id" not in pkg_dict or "resources" not in pkg_dict: + log.info( + "Attempt to view unmigrated or badly migrated dataset " + "{} {}".format(id, activity_id) + ) + tk.abort( + 404, tk._("The detail of this dataset activity is not available") + ) + if pkg_dict["id"] != current_pkg["id"]: + log.info( + "Mismatch between pkg id in activity and URL {} {}".format( + pkg_dict["id"], current_pkg["id"] + ) + ) + # the activity is not for the package in the URL - don't allow + # misleading URLs as could be malicious + tk.abort(404, tk._("Activity not found")) + # The name is used lots in the template for links, so fix it to be + # the current one. It's not displayed to the user anyway. + pkg_dict["name"] = current_pkg["name"] + + # Earlier versions of CKAN only stored the package table in the + # activity, so add a placeholder for resources, or the template + # will crash. + pkg_dict.setdefault("resources", []) + + # can the resources be previewed? + for resource in pkg_dict["resources"]: + resource_views = tk.get_action("resource_view_list")( + context, {"id": resource["id"]} + ) + resource["has_views"] = len(resource_views) > 0 + + package_type = pkg_dict["type"] or "dataset" + _setup_template_variables(context, {"id": id}, package_type=package_type) + + return tk.render( + "package/history.html", + { + "dataset_type": package_type, + "pkg_dict": pkg_dict, + "pkg": pkg, + }, + ) + + +@bp.route("/dataset/activity/") +def package_activity(id: str) -> Union[Response, str]: # noqa + """Render this package's public activity stream page.""" + after = tk.request.args.get("after") + before = tk.request.args.get("before") + activity_type = tk.request.args.get("activity_type") + + context = cast( + Context, + { + "for_view": True, + "auth_user_obj": tk.g.userobj, + }, + ) + + data_dict = {"id": id} + limit = _get_activity_stream_limit() + activity_types = [activity_type] if activity_type else None + + try: + pkg_dict = tk.get_action("package_show")(context, data_dict) + activity_dict = { + "id": pkg_dict["id"], + "after": after, + "before": before, + # ask for one more just to know if this query has more results + "limit": limit + 1, + "activity_types": activity_types, + } + activity_stream = tk.get_action("package_activity_list")( + context, activity_dict + ) + dataset_type = pkg_dict["type"] or "dataset" + except tk.ObjectNotFound: + return tk.abort(404, tk._("Dataset not found")) + except tk.NotAuthorized: + return tk.abort(403, tk._("Unauthorized to read dataset %s") % id) + except tk.ValidationError: + return tk.abort(400, tk._("Invalid parameters")) + + has_more = len(activity_stream) > limit + # remove the extra item if exists + if has_more: + if after: + activity_stream.pop(0) + else: + activity_stream.pop() + + older_activities_url = _get_older_activities_url( + has_more, + activity_stream, + id=id, + activity_type=activity_type + ) + + newer_activities_url = _get_newer_activities_url( + has_more, + activity_stream, + id=id, + activity_type=activity_type + ) + + return tk.render( + "package/activity_stream.html", + { + "dataset_type": dataset_type, + "pkg_dict": pkg_dict, + "activity_stream": activity_stream, + "id": id, # i.e. package's current name + "limit": limit, + "has_more": has_more, + "activity_type": activity_type, + "activity_types": VALIDATORS_PACKAGE_ACTIVITY_TYPES.keys(), + "newer_activities_url": newer_activities_url, + "older_activities_url": older_activities_url, + }, + ) + + +@bp.route("/dataset/changes/") +def package_changes(id: str) -> Union[Response, str]: # noqa + """ + Shows the changes to a dataset in one particular activity stream item. + """ + activity_id = id + context = cast(Context, {"auth_user_obj": tk.g.userobj}) + try: + activity_diff = tk.get_action("activity_diff")( + context, + {"id": activity_id, "object_type": "package", "diff_type": "html"}, + ) + except tk.ObjectNotFound as e: + log.info("Activity not found: {} - {}".format(str(e), activity_id)) + return tk.abort(404, tk._("Activity not found")) + except tk.NotAuthorized: + return tk.abort(403, tk._("Unauthorized to view activity data")) + + # 'pkg_dict' needs to go to the templates for page title & breadcrumbs. + # Use the current version of the package, in case the name/title have + # changed, and we need a link to it which works + pkg_id = activity_diff["activities"][1]["data"]["package"]["id"] + current_pkg_dict = tk.get_action("package_show")(context, {"id": pkg_id}) + pkg_activity_list = tk.get_action("package_activity_list")( + context, {"id": pkg_id, "limit": 100} + ) + + return tk.render( + "package/changes.html", + { + "activity_diffs": [activity_diff], + "pkg_dict": current_pkg_dict, + "pkg_activity_list": pkg_activity_list, + "dataset_type": current_pkg_dict["type"], + }, + ) + + +@bp.route("/dataset/changes_multiple") +def package_changes_multiple() -> Union[Response, str]: # noqa + """ + Called when a user specifies a range of versions they want to look at + changes between. Verifies that the range is valid and finds the set of + activity diffs for the changes in the given version range, then + re-renders changes.html with the list. + """ + + new_id = tk.h.get_request_param("new_id") + old_id = tk.h.get_request_param("old_id") + + context = cast(Context, {"auth_user_obj": tk.g.userobj}) + + # check to ensure that the old activity is actually older than + # the new activity + old_activity = tk.get_action("activity_show")( + context, {"id": old_id, "include_data": False} + ) + new_activity = tk.get_action("activity_show")( + context, {"id": new_id, "include_data": False} + ) + + old_timestamp = old_activity["timestamp"] + new_timestamp = new_activity["timestamp"] + + t1 = datetime.strptime(old_timestamp, "%Y-%m-%dT%H:%M:%S.%f") + t2 = datetime.strptime(new_timestamp, "%Y-%m-%dT%H:%M:%S.%f") + + time_diff = t2 - t1 + # if the time difference is negative, just return the change that put us + # at the more recent ID we were just looking at + # TODO: do something better here - go back to the previous page, + # display a warning that the user can't look at a sequence where + # the newest item is older than the oldest one, etc + if time_diff.total_seconds() <= 0: + return package_changes(tk.h.get_request_param("current_new_id")) + + done = False + current_id = new_id + diff_list = [] + + while not done: + try: + activity_diff = tk.get_action("activity_diff")( + context, + { + "id": current_id, + "object_type": "package", + "diff_type": "html", + }, + ) + except tk.ObjectNotFound as e: + log.info("Activity not found: {} - {}".format(str(e), current_id)) + return tk.abort(404, tk._("Activity not found")) + except tk.NotAuthorized: + return tk.abort(403, tk._("Unauthorized to view activity data")) + + diff_list.append(activity_diff) + + if activity_diff["activities"][0]["id"] == old_id: + done = True + else: + current_id = activity_diff["activities"][0]["id"] + + pkg_id: str = diff_list[0]["activities"][1]["data"]["package"]["id"] + current_pkg_dict = tk.get_action("package_show")(context, {"id": pkg_id}) + pkg_activity_list = tk.get_action("package_activity_list")( + context, {"id": pkg_id, "limit": 100} + ) + + return tk.render( + "package/changes.html", + { + "activity_diffs": diff_list, + "pkg_dict": current_pkg_dict, + "pkg_activity_list": pkg_activity_list, + "dataset_type": current_pkg_dict["type"], + }, + ) + + +@bp.route( + "/group/activity/", + endpoint="group_activity", + defaults={"group_type": "group"}, +) +@bp.route( + "/organization/activity/", + endpoint="organization_activity", + defaults={"group_type": "organization"}, +) +def group_activity(id: str, group_type: str) -> str: + """Render this group's public activity stream page.""" + after = tk.request.args.get("after") + before = tk.request.args.get("before") + + if group_type == 'organization': + set_org(True) + + context = cast(Context, {"user": tk.g.user, "for_view": True}) + + try: + group_dict = _get_group_dict(id, group_type) + except (tk.ObjectNotFound, tk.NotAuthorized): + tk.abort(404, tk._("Group not found")) + + real_group_type = group_dict["type"] + action_name = "organization_activity_list" + if not group_dict.get("is_organization"): + action_name = "group_activity_list" + + activity_type = tk.request.args.get("activity_type") + activity_types = [activity_type] if activity_type else None + + limit = _get_activity_stream_limit() + + try: + activity_stream = tk.get_action(action_name)( + context, { + "id": group_dict["id"], + "before": before, + "after": after, + "limit": limit + 1, + "activity_types": activity_types + } + ) + except tk.ValidationError as error: + tk.abort(400, error.message or "") + + filter_types = VALIDATORS_PACKAGE_ACTIVITY_TYPES.copy() + if group_type == 'organization': + filter_types.update(VALIDATORS_ORGANIZATION_ACTIVITY_TYPES) + else: + filter_types.update(VALIDATORS_GROUP_ACTIVITY_TYPES) + + has_more = len(activity_stream) > limit + # remove the extra item if exists + if has_more: + if after: + activity_stream.pop(0) + else: + activity_stream.pop() + + older_activities_url = _get_older_activities_url( + has_more, + activity_stream, + id=id, + activity_type=activity_type + ) + + newer_activities_url = _get_newer_activities_url( + has_more, + activity_stream, + id=id, + activity_type=activity_type + ) + + extra_vars = { + "id": id, + "activity_stream": activity_stream, + "group_type": real_group_type, + "group_dict": group_dict, + "activity_type": activity_type, + "activity_types": filter_types.keys(), + "newer_activities_url": newer_activities_url, + "older_activities_url": older_activities_url + } + + return tk.render( + _get_group_template("activity_template", group_type), extra_vars + ) + + +@bp.route( + "/group/changes/", + defaults={"is_organization": False, "group_type": "group"}, +) +@bp.route( + "/organization/changes/", + endpoint="organization_changes", + defaults={"is_organization": True, "group_type": "organization"}, +) +def group_changes(id: str, group_type: str, is_organization: bool) -> str: + """ + Shows the changes to an organization in one particular activity stream + item. + """ + extra_vars = {} + activity_id = id + context = cast( + Context, + { + "auth_user_obj": tk.g.userobj, + }, + ) + try: + activity_diff = tk.get_action("activity_diff")( + context, + {"id": activity_id, "object_type": "group", "diff_type": "html"}, + ) + except tk.ObjectNotFound as e: + log.info("Activity not found: {} - {}".format(str(e), activity_id)) + return tk.abort(404, tk._("Activity not found")) + except tk.NotAuthorized: + return tk.abort(403, tk._("Unauthorized to view activity data")) + + # 'group_dict' needs to go to the templates for page title & breadcrumbs. + # Use the current version of the package, in case the name/title have + # changed, and we need a link to it which works + group_id = activity_diff["activities"][1]["data"]["group"]["id"] + current_group_dict = tk.get_action(group_type + "_show")( + context, {"id": group_id} + ) + group_activity_list = tk.get_action(group_type + "_activity_list")( + context, {"id": group_id, "limit": 100} + ) + + extra_vars: dict[str, Any] = { + "activity_diffs": [activity_diff], + "group_dict": current_group_dict, + "group_activity_list": group_activity_list, + "group_type": current_group_dict["type"], + } + + return tk.render(_replace_group_org("group/changes.html"), extra_vars) + + +@bp.route( + "/group/changes_multiple", + defaults={"is_organization": False, "group_type": "group"}, +) +@bp.route( + "/organization/changes_multiple", + endpoint="organization_changes_multiple", + defaults={"is_organization": True, "group_type": "organization"}, +) +def group_changes_multiple(is_organization: bool, group_type: str) -> str: + """ + Called when a user specifies a range of versions they want to look at + changes between. Verifies that the range is valid and finds the set of + activity diffs for the changes in the given version range, then + re-renders changes.html with the list. + """ + extra_vars = {} + new_id = tk.h.get_request_param("new_id") + old_id = tk.h.get_request_param("old_id") + + context = cast( + Context, + { + "auth_user_obj": tk.g.userobj, + }, + ) + + # check to ensure that the old activity is actually older than + # the new activity + old_activity = tk.get_action("activity_show")( + context, {"id": old_id, "include_data": False} + ) + new_activity = tk.get_action("activity_show")( + context, {"id": new_id, "include_data": False} + ) + + old_timestamp = old_activity["timestamp"] + new_timestamp = new_activity["timestamp"] + + t1 = datetime.strptime(old_timestamp, "%Y-%m-%dT%H:%M:%S.%f") + t2 = datetime.strptime(new_timestamp, "%Y-%m-%dT%H:%M:%S.%f") + + time_diff = t2 - t1 + # if the time difference is negative, just return the change that put us + # at the more recent ID we were just looking at + # TODO: do something better here - go back to the previous page, + # display a warning that the user can't look at a sequence where + # the newest item is older than the oldest one, etc + if time_diff.total_seconds() < 0: + return group_changes( + tk.h.get_request_param("current_new_id"), + group_type, + is_organization, + ) + + done = False + current_id = new_id + diff_list = [] + + while not done: + try: + activity_diff = tk.get_action("activity_diff")( + context, + { + "id": current_id, + "object_type": "group", + "diff_type": "html", + }, + ) + except tk.ObjectNotFound as e: + log.info("Activity not found: {} - {}".format(str(e), current_id)) + return tk.abort(404, tk._("Activity not found")) + except tk.NotAuthorized: + return tk.abort(403, tk._("Unauthorized to view activity data")) + + diff_list.append(activity_diff) + + if activity_diff["activities"][0]["id"] == old_id: + done = True + else: + current_id = activity_diff["activities"][0]["id"] + + group_id: str = diff_list[0]["activities"][1]["data"]["group"]["id"] + current_group_dict = tk.get_action(group_type + "_show")( + context, {"id": group_id} + ) + group_activity_list = tk.get_action(group_type + "_activity_list")( + context, {"id": group_id, "limit": 100} + ) + + extra_vars: dict[str, Any] = { + "activity_diffs": diff_list, + "group_dict": current_group_dict, + "group_activity_list": group_activity_list, + "group_type": current_group_dict["type"], + } + + return tk.render(_replace_group_org("group/changes.html"), extra_vars) + + +@bp.route("/user/activity/") +def user_activity(id: str) -> str: + """Render this user's public activity stream page.""" + after = tk.request.args.get("after") + before = tk.request.args.get("before") + + context = cast( + Context, + { + "auth_user_obj": tk.g.userobj, + "for_view": True, + }, + ) + data_dict: dict[str, Any] = { + "id": id, + "user_obj": tk.g.userobj, + "include_num_followers": True, + } + try: + tk.check_access("user_show", context, data_dict) + except tk.NotAuthorized: + tk.abort(403, tk._("Not authorized to see this page")) + + extra_vars = _extra_template_variables(context, data_dict) + + limit = _get_activity_stream_limit() + + try: + activity_stream = tk.get_action( + "user_activity_list" + )(context, { + "id": extra_vars["user_dict"]["id"], + "before": before, + "after": after, + "limit": limit + 1, + }) + except tk.ValidationError: + tk.abort(400) + + has_more = len(activity_stream) > limit + # remove the extra item if exists + if has_more: + if after: + activity_stream.pop(0) + else: + activity_stream.pop() + + older_activities_url = _get_older_activities_url( + has_more, + activity_stream, + id=id + ) + + newer_activities_url = _get_newer_activities_url( + has_more, + activity_stream, + id=id + ) + + extra_vars.update({ + "id": id, + "activity_stream": activity_stream, + "newer_activities_url": newer_activities_url, + "older_activities_url": older_activities_url + }) + + return tk.render("user/activity_stream.html", extra_vars) + + +@bp.route("/dashboard/", strict_slashes=False) +def dashboard() -> str: + context = cast( + Context, + { + "auth_user_obj": tk.g.userobj, + "for_view": True, + }, + ) + data_dict: dict[str, Any] = {"user_obj": tk.g.userobj} + extra_vars = _extra_template_variables(context, data_dict) + + q = tk.request.args.get("q", "") + filter_type = tk.request.args.get("type", "") + filter_id = tk.request.args.get("name", "") + before = tk.request.args.get("before") + after = tk.request.args.get("after") + + limit = _get_activity_stream_limit() + + extra_vars["followee_list"] = tk.get_action("followee_list")( + context, {"id": tk.g.userobj.id, "q": q} + ) + extra_vars["dashboard_activity_stream_context"] = _get_dashboard_context( + filter_type, filter_id, q + ) + activity_stream = tk.h.dashboard_activity_stream( + tk.g.userobj.id, + filter_type=filter_type, + filter_id=filter_id, + limit=limit + 1, + before=before, + after=after + ) + + has_more = len(activity_stream) > limit + # remove the extra item if exists + if has_more: + if after: + activity_stream.pop(0) + else: + activity_stream.pop() + + older_activities_url = _get_older_activities_url( + has_more, + activity_stream, + type=filter_type, + name=filter_id + ) + + newer_activities_url = _get_newer_activities_url( + has_more, + activity_stream, + type=filter_type, + name=filter_id + ) + + extra_vars.update({ + "id": id, + "dashboard_activity_stream": activity_stream, + "newer_activities_url": newer_activities_url, + "older_activities_url": older_activities_url + }) + + # Mark the user's new activities as old whenever they view their + # dashboard page. + tk.get_action("dashboard_mark_activities_old")(context, {}) + + return tk.render("user/dashboard.html", extra_vars) + + +def _get_dashboard_context( + filter_type: Optional[str] = None, + filter_id: Optional[str] = None, + q: Optional[str] = None, +) -> dict[str, Any]: + """Return a dict needed by the dashboard view to determine context.""" + + def display_name(followee: dict[str, Any]) -> Optional[str]: + """Return a display name for a user, group or dataset dict.""" + display_name = followee.get("display_name") + fullname = followee.get("fullname") + title = followee.get("title") + name = followee.get("name") + return display_name or fullname or title or name + + if filter_type and filter_id: + context = cast( + Context, + { + "auth_user_obj": tk.g.userobj, + "for_view": True, + }, + ) + data_dict: dict[str, Any] = { + "id": filter_id, + "include_num_followers": True, + } + followee = None + + action_functions = { + "dataset": "package_show", + "user": "user_show", + "group": "group_show", + "organization": "organization_show", + } + action_name = action_functions.get(filter_type) + if action_name is None: + tk.abort(404, tk._("Follow item not found")) + + action_function = tk.get_action(action_name) + try: + followee = action_function(context, data_dict) + except (tk.ObjectNotFound, tk.NotAuthorized): + tk.abort(404, tk._("{0} not found").format(filter_type)) + + if followee is not None: + return { + "filter_type": filter_type, + "q": q, + "context": display_name(followee), + "selected_id": followee.get("id"), + "dict": followee, + } + + return { + "filter_type": filter_type, + "q": q, + "context": tk._("Everything"), + "selected_id": False, + "dict": None, + } diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/audioview/__init__.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/audioview/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/audioview/plugin.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/audioview/plugin.py new file mode 100644 index 0000000..e827961 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/audioview/plugin.py @@ -0,0 +1,50 @@ +# encoding: utf-8 +from __future__ import annotations + +from typing import Any + +import ckan.plugins as p +from ckan.types import Context, DataDict +from ckan.common import CKANConfig +from ckan.config.declaration import Declaration, Key + +ignore_empty = p.toolkit.get_validator('ignore_empty') +unicode_safe = p.toolkit.get_validator('unicode_safe') + + +class AudioView(p.SingletonPlugin): + '''This plugin makes views of audio resources, using an link to the audio instead. + {% endtrans %} + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/__init__.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/plugin.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/plugin.py new file mode 100644 index 0000000..5b0bc57 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/plugin.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +from ckan.types import Action, AuthFunction, Context, DataDict +from typing import Any, Callable, Optional +import ckan.plugins as p +from ckan.plugins.toolkit import (auth_allow_anonymous_access, + chained_auth_function, + chained_action, + side_effect_free, + chained_helper + ) + + +class ChainedFunctionsPlugin(p.SingletonPlugin): + p.implements(p.IAuthFunctions) + p.implements(p.IActions) + p.implements(p.ITemplateHelpers) + + def get_auth_functions(self): + return { + "user_show": user_show + } + + def get_actions(self): + return { + "package_search": package_search + } + + def get_helpers(self) -> dict[str, Callable[..., Any]]: + return { + "ckan_version": ckan_version + } + + +@auth_allow_anonymous_access +@chained_auth_function +def user_show(next_auth: AuthFunction, context: Context, + data_dict: Optional[DataDict] = None): + return next_auth(context, data_dict) # type: ignore + + +@side_effect_free +@chained_action +def package_search(original_action: Action, context: Context, + data_dict: DataDict): + return original_action(context, data_dict) + + +@chained_helper +def ckan_version(next_func: Callable[..., Any], **kw: Any): + return next_func(**kw) + + +setattr(ckan_version, "some_attribute", "some_value") diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/tests/__init__.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/tests/test_plugin.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/tests/test_plugin.py new file mode 100644 index 0000000..5e20737 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/chained_functions/tests/test_plugin.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +import pytest + + +@pytest.mark.ckan_config(u"ckan.plugins", u"chained_functions") +@pytest.mark.usefixtures(u"with_plugins") +class TestChainedFunctionsPlugin(object): + def test_auth_attributes_are_retained(self): + from ckan.authz import _AuthFunctions + user_show = _AuthFunctions.get("user_show") + assert hasattr(user_show, 'auth_allow_anonymous_access') is True + + def test_action_attributes_are_retained(self): + from ckan.plugins.toolkit import get_action + package_search = get_action('package_search') + assert hasattr(package_search, 'side_effect_free') is True + + def test_helper_attributes_are_retained(self): + from ckan.lib.helpers import helper_functions + ckan_version = helper_functions.get('ckan_version') + assert hasattr(ckan_version, "some_attribute") is True diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/__init__.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/__init__.py new file mode 100644 index 0000000..6cca9de --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/__init__.py @@ -0,0 +1,19 @@ +# from flask import Flask, g +# from ckan.lib.app_globals import app_globals +# import ckan.plugins.toolkit as toolkit + +# app = Flask("d4Science") + +# def base_context(): +# return { +# 'helpers': toolkit.h, +# } + +# @app.context_processor +# def inject_base_context(): +# return base_context() + +# # Aggiungi main_css al contesto globale di Flask +# # @app.before_request +# # def before_request(): +# # g.main_css = app_globals.main_css \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/assets/css/d4science_theme.css b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/assets/css/d4science_theme.css new file mode 100644 index 0000000..9e416b1 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/assets/css/d4science_theme.css @@ -0,0 +1,844 @@ +/* ===================================================== + The "account masthead" bar across the top of the site + ===================================================== */ + +.account-masthead { + background-color: #ccc; +} +/* The "bubble" containing the number of new notifications. */ +.account-masthead .account .notifications a span { + background-color: #9fa0a2; +} +/* The text and icons in the user account info. */ +.account-masthead .account ul li a { + color: rgba(255, 255, 255, 0.6); +} +/* The user account info text and icons, when the user's pointer is hovering + over them. */ +.account-masthead .account ul li a:hover { + color: rgba(255, 255, 255, 0.7); +/* background-color: black;*/ +} + + +/* ======================================================================== + The main masthead bar that contains the site logo, nav links, and search + ======================================================================== */ + +.masthead { + background: #eee url("/bg-noise.png") repeat scroll 0 0; + border-top: 1px solid #555; + padding-top: 5px; + padding-bottom: 15px !important; + border-bottom: 1px solid #999; +/* background-image: url("/bg-pattern.min.svg") !important; */ +} + +.masthead .navigation .nav-pills li a{ + color: #187794; +} + +a.logo > img{ + margin-bottom: 5px; +} + +/* The "navigation pills" in the masthead (the links to Datasets, + Organizations, etc) when the user's pointer hovers over them. */ +.masthead .navigation .nav-pills li a:hover { +/* background-color: rgb(48, 48, 48);*/ + color: white; +} +/* The "active" navigation pill (for example, when you're on the /dataset page + the "Datasets" link is active). */ +.masthead .navigation .nav-pills li.active a { + background-color: #d2d2d5; +} +/* The "box shadow" effect that appears around the search box when it + has the keyboard cursor's focus. */ +.masthead input[type="text"]:focus { + -webkit-box-shadow: inset 0px 0px 2px 0px rgba(0, 0, 0, 0.7); + box-shadow: inset 0px 0px 2px 0px rgba(0, 0, 0, 0.7); +} + + +/* =========================================== + The content in the middle of the front page + =========================================== */ + +/* Remove the "box shadow" effect around various boxes on the page. */ +.box { + box-shadow: none; +} +.hero { + background: #FEFEFE repeat scroll 0 0 !important; +} +/* Remove the borders around the "Welcome to CKAN" and "Search Your Data" + boxes. */ +.hero .box { + /*border: none;*/ + margin-top: 10px !important; +} +/* Change the colors of the "Search Your Data" box. */ +.homepage .module-search .module-content { + color: rgb(68, 68, 68); + background-color: white; +} +/* Change the background color of the "Popular Tags" box. */ +.homepage .module-search .tags { + background-color: #fcfcfc; +} + +.homepage-title{ + font-size: 20px; + font-weight: bold; + color: #202020; + margin-bottom: 20px; +} + +/* Change the background color of the "Popular Tags" box. */ +.homepage .module-search h3{ + color: #444; +} + +/* Remove some padding. This makes the bottom edges of the "Welcome to CKAN" + and "Search Your Data" boxes line up. */ +.module-content:last-child { + /*padding-bottom: 0px;*/ +} +.homepage .module-search { + padding: 0px; +} +/* Add a border line between the top and bottom halves of the front page. */ +.homepage [role="main"] { + border-bottom: 1px solid #bbb; + padding: 10px 0; +} + +.homepage .stats ul li a b{ + font-size: 30px !important; +} + +[role="main"], .main { +/* background: #f5f6fa url("/bg-pattern.min.svg") repeat; scroll 0 0;*/ + /*background: #fafafa url("/bg-pattern.svg") repeat; scroll 0 0;*/ + background: #fdfdfd none repeat scroll 0 0; + min-height: 0px !important; +} + +.media-item-homepage { + background-color: white; + border-radius: 3px; + float: left; + margin: 15px 0 0 15px; + overflow: hidden; + padding-left: 10px; + padding-right: 10px; + position: relative; + text-align: center; + width: 150px; +} + +.media-heading-homepage { + font-size: 16px; + hyphens: auto; + line-height: 1.3; + margin: 5px 0; +} + +.media-grid-homepage { + -moz-border-bottom-colors: none; + -moz-border-left-colors: none; + -moz-border-right-colors: none; + -moz-border-top-colors: none; +/* background: #fbfbfb url("../../../base/images/bg.png") repeat scroll 0 0; + border-color: #dddddd; + border-image: none; + border-style: solid; + border-width: 1px 0;*/ + list-style: outside none none; + margin: 0 -10px; + padding-bottom: 15px; +} +.media-grid-homepage::before, .media-grid::after { + content: ""; + display: table; + line-height: 0; +} +.media-grid-homepage::after { + clear: both; +} + +.background-circle{ + padding: 10px 10px; + display: inline-block !important; + -webkit-border-radius: 90px; + -moz-border-radius: 90px; + border-radius: 90px; + background-color: #4679b2; + text-decoration: none !important; +} + +.color-white{ + color: white !important; +} + +.badge-circle { + border-radius: 50% 50% 50% 50% !important; + height: 60px; + text-align: center; + vertical-align: middle; + width: 65px; + background-color: #4679b2; + display: inline-block !important; + padding-top: 5px; + text-decoration: none !important; +} + +/* ==================================== + The footer at the bottom of the site + ==================================== */ + +.site-footer, body { + background-color: #bbb; + font-family: "Lato","Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 16px; +} +/* The text in the footer. */ +.site-footer, +.site-footer label, +.site-footer small { + color: rgba(255, 255, 255, 0.6); +} +/* The link texts in the footer. */ +.site-footer a { + color: rgba(255, 255, 255, 0.6); +} + +.site-footer-internal{ + min-height: 10px; + padding: 2px 0; + font-size: 12px; +} + +.site-footer-internal { + /*background-color: rgba(255, 255, 255, 0.6);*/ + text-align: center; + /*display: inline-block;*/ +} + +.site-footer-internal, +.site-footer-internal label, +.site-footer-internal small { + +} + +.site-footer-internal a { + display: inline-block; +} + +.d4s-hide-text { + background-color: transparent; + border: 0 none; + color: transparent; + font: 0px/0 a; + text-shadow: none; +} + +.d4science-footer-logo { + background: url("/gCube_70.png") no-repeat scroll left top rgba(0, 0, 0, 0); + height: 32px; + margin-top: 2px; + text-indent: -900em; + width: 75px; +} + +.d4s-ckan-footer-logo { + background: rgba(0, 0, 0, 0) url("/ckan-logo-footer.png") no-repeat scroll left top; + height: 21px; + margin-top: 2px; + text-indent: -900em; + width: 69px; +} + +.site-footer-d4science { + font-size: 14px; + color: #f5f5f5; + text-align: center; + height: 25px; + padding-top: 5px; + background-color: #7F7F7F; +} + +.site-footer-d4science a { + font-weight: bold; + text-decoration: none; + color: white; +} + + +/* ==================================== + Base elements of the site + ==================================== */ + +div .principaltitle { + color: inherit; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 20px; + font-weight: bold; + line-height: 1.2; + margin: 15px 0; + text-rendering: optimizelegibility; + word-break: break-all; + padding-bottom: 10px; + padding-top: 5px; + border-bottom: 1px solid #eee; +} + +div .notes { + color: #444444; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 14px; + line-height: 1.3; + text-align: justify; + word-break: break-all; +} + +div .infotitle { + font-size: 15px; + hyphens: auto; + line-height: 1.3; + word-break: break-all; + font-weight: bold; +} + +.toolbar .breadcrumb{ + font-size: 16px !important; +} + +.box{ + border: 0px !important; +} + +div .sectiontitle{ + color: #9F9F9F; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 17px; + font-weight: bold; + margin: 20px 0; + margin-top: 20px; + margin-bottom: 10px; + text-rendering: optimizelegibility; +} + +section .well { + background-color: #fdfdfd !important; + border: 1px solid #e3e3e3; + border-radius: 4px; + box-shadow: none !important; + margin-bottom: 20px; + min-height: 20px; + +} + +.page-heading { + font-size: 18px; + line-height: 1.2; + margin-top: 20px; + margin-bottom: 0px; +} + +#dataset-resources .resource-list{ + background-color: #fdfdfd !important; + border: 1px solid #e3e3e3; + border-radius: 4px; + box-shadow: none !important; + margin: -1px 0 !important; +} + +.wrapper{ + border: 1px solid #d0d0d0; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05); + border-radius: 3px +} + +.home-popular{ + padding-top: 25px; +} + +.logo-homepage{ + max-height: 60px; +} + +.statistics-show{ + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + color: #444444; + text-decoration: none; +} + +.d4s-center-cropped{ + text-align: center; + background-color: #eee; + border: 1px solid #ddd; + padding-bottom: 10px; + padding-top: 10px; +} + +.tag-list { + font-size: 14px; +} + + +/* ==================================== + Acquired Dataset + ==================================== */ +.label-acquired { + background-color: #55a1ce; +} + +.label-owner { + background-color: #e0051e; +} + +.divider { + margin-left:10px; + height:auto; + display:inline-block; +} + +/* ==================================== + List Dataset + ==================================== */ + +/*LEFT +.show_meatadatatype { + color: white; + display: inline-block; // Inline elements with width and height. TL;DR they make the icon buttons stack from left-to-right instead of top-to-bottom + position: relative; // All 'absolute'ly positioned elements are relative to this one + margin-bottom: 20px; + margin-left: 25px; +} +*/ + +/*RIGHT*/ +.show_meatadatatype { + color: white; + display: inline-block; + float: right; + margin-right: 2px; + margin-top: -20px; + position: relative; +} + + + +/* LEFT + * Position the badge within the relatively positioned button +.button__badge { + background-color: #fa3e3e; + border-radius: 2px; + color: white; + + padding: 1px 6px; + font-size: 10px; + + position: absolute; + top: 0; + right: 0; +}*/ + + + +/* RIGTH */ +.button__badge { + color: #808080; + padding: 0px 2px; + font-size: 10px; + top: 0; + right: 0; + font-family: sans-serif, times, georgia; +} + +/* ==================================== + Modal Popup + ==================================== */ + +/* Popup container - can be anything you want */ +/* The Modal (background) */ +.d4s_modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 10001; /* Sit on top (NB. At 1000 there is the zoom in/out of the Map Widget)*/ + /*padding-top: 100px;*/ /* Location of the box */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ +} + +/* Modal Content */ +.d4s_modal-content { + background-color: #fefefe; + /*margin: auto;*/ + padding: 20px; + border: 1px solid #888; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + position: absolute; + left: 50%; + margin-left: -225px; + width: 450px; +} + +/* The Close Button */ +.d4s_close { + color: #aaaaaa; + float: right; + font-size: 28px; + font-weight: bold; + padding-left: 20px; +} + +.d4s_close:hover, +.d4s_close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +.d4s_div_clickable{ + cursor: pointer; +} + +/*==================================== +D4S POPUP +======================================*/ + +/* Popup container - can be anything you want */ +.popupD4SNoArrow { + position: relative; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* The actual popup */ +.popupD4SNoArrow .popuptext { + visibility: hidden; + width: 300px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 8px; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + margin-left: -150px; +} + +/* Toggle this class - hide and show the popup */ +.popupD4SNoArrow .show { + visibility: visible; + -webkit-animation: fadeIn 1s; + animation: fadeIn 1s; +} + + +/* Popup container - can be anything you want */ +.popupD4S { + position: relative; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* The actual popup */ +.popupD4S .popuptext { + visibility: hidden; + width: 300px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 8px; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + margin-left: -150px; +} + +/* Popup arrow */ +.popupD4S .popuptext::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #555 transparent transparent transparent; +} + +/* Toggle this class - hide and show the popup */ +.popupD4S .show { + visibility: visible; + -webkit-animation: fadeIn 1s; + animation: fadeIn 1s; +} + +/* Add animation (fade in the popup) */ +@-webkit-keyframes fadeIn { + from {opacity: 0;} + to {opacity: 1;} +} + +@keyframes fadeIn { + from {opacity: 0;} + to {opacity:1 ;} +} + +/*==================================== +D4S PACKAGE +======================================*/ + +.graphic-preview-style { + text-align: center; + border-top: 1px dotted #DDD; + padding-top: 10px; + padding-bottom: 0px; + margin-top: 15px; +} + +.graphic-preview-style a{ + font-size: 13px; +} + +.graphic-preview-style img{ + max-width: 100% !important; + height: auto; + +} + +.graphic-preview-style #graphic-title{ + font-size: 13px; + +} + +.nav-item{ + word-wrap:break-word; + } + +/*==================================== +RESOURCE_LIST RESOURCE_ITEM INTO PACKAGE +======================================*/ + +.required-access { + font-style: italic; + font-weight: bold; + padding: 5px; +} + +/*==================================== +LINK TO RESOURCES FROM PACKAGE LIST +======================================*/ + +.dataset-resources li a { + background-color: #187794; +} + +.label[data-format="csw"], .label[data-format*="csw"] { + background-color: #e6b800; +} + +/*==================================== +CSS APPLIED TO Similar GRSF Records +======================================*/ + +.my-grsf-table{ + word-break: break-all; +} + +.my-grsf-table tr td{ + width: inherit; +} + +.my-grsf-table tr td:first-child{ + width: 82px !important; +} + +/*==================================== +CSS APPLIED in base.html +======================================*/ + +#ckan-page-loading { + display: none; + position: fixed; + top: 50%; + left: 50%; + margin-top: -130px; + margin-left: -130px; + width: 260px; + height: 260px; + z-index: 100000; + background-image: url("/pageloading.gif"); + background-repeat: no-repeat; + background-position: center; +} + +/*==================================== +CSS APPLIED in search_for_location.html +======================================*/ + +div#search-for-location { + +} + +div#search-for-location #dataset-map { + position: relative !important; + top: +0px !important; +} + +div#search-for-location #dataset-map-container { + height: 300px; +} + + +div#search-for-location .module-heading { + display: none; +} + +div#search-for-extent{ + padding-top: 10px; +} + +/*==================================== +CSS APPLIED in additional_info.html +======================================*/ +.qr-code-table { + width: 100%; +} + +.qr-code-table td { + width: 85%; + border: 1px solid #e3e3e3; +} + +.qr-code-table td:first-child { + padding-left: 10px; + border-right-style: none; + +} + +.qr-code-table td:last-child { + width: 105px; + text-align: center; + border-left-style: none; + +} + +/* MAX-WITH APPIED TO QR_CODE */ +.qr-code-table img { + max-width: 100px; + height: auto; +} + + +/*==================================== +CSS APPLIED FROM JSON TO HTML TABLE +======================================*/ + +.json-to-html-table-column{ + word-break: break-all; +} + +.json-to-html-table-column tr td{ + width: inherit; +} + +.json-to-html-table-column tr td:first-child{ + font-weight: bold; + color: #5a5a5a; +} + +/*==================================== +CSS APPLIED into custom_form_fields +======================================*/ +.disabled-div{ + pointer-events: none; + opacity: 0.5; +} + +.disabled-div input[type="text"]{ + background: #f1f1f1; +} + +/*==================================== +CSS APPLIED into extra_table.html +======================================*/ + +.read-more-state { + display: none; +} + +.read-more-target { + opacity: 0; + max-height: 0; + font-size: 0; + transition: .25s ease; +} + +.read-more-state:checked ~ .read-more-wrap .read-more-target { + opacity: 1; + font-size: inherit; + max-height: 999em; + content: ""; +} + +.read-more-state ~ .read-more-trigger:before { + content: 'Show more'; +} + +.read-more-state:checked ~ .read-more-trigger:before { + content: 'Show less'; +} + +.read-more-state:checked ~ .read-more-wrap::after { + content: ""; +} + +.read-more-trigger { + cursor: pointer; + display: inline-block; + padding: 0 .5em; + color: #187794; + font-size: .9em; + line-height: 2; + border: 1px solid #ddd; + border-radius: .25em; + font-weight: normal; +} + +.read-more-trigger::after { + content: ""; +} + +.read-more-wrap { + margin-bottom: 2px; +} + +.read-more-wrap::after{ + content: " ..."; + +} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/assets/js/d4science_scripts.js b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/assets/js/d4science_scripts.js new file mode 100644 index 0000000..36b79e3 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/assets/js/d4science_scripts.js @@ -0,0 +1,305 @@ +/* ===================================================== +JavaScript used by CKAN plugin: 'd4science_theme' +Created by Francesco Mangiacrapa ISTI-CNR Pisa, Italy +===================================================== */ + + +//old script starts +CKAN_D4S_Breadcrumb_Manager = { + + breadcrumbShow : function (show) { + + var breadcrumb = document.getElementById('ckan-breadcrumb'); + console.log('breadcrumb is '+breadcrumb) + if(breadcrumb){ + if(show){ + breadcrumb.style.display = 'block'; + this.organizationTreeShow(true); + } else{ + breadcrumb.style.display = 'none'; + this.organizationTreeShow(false); + } + } + + //var elements = document.getElementsByTagName('a'); + //for(var i = 0, len = elements.length; i < len; i++) { + // elements[i].onclick = function () { + // //alert("You clicked an external link to: " + this.href); + // //window.parent.add_hide_breadcrumb_to_dom(false); + // this.add_hide_breadcrumb_to_dom(false); + // } + //} + }, + + organizationTreeShow : function (show) { + var trees = document.getElementsByClassName("hierarchy-tree-top"); + + if (trees){ + for (i = 0; i < trees.length; i++) { + if(show){ + trees[i].style.display = 'block'; + } else{ + trees[i].style.display = 'none'; + } + } + } + }, + + checkBreadcrumbShow : function () { + + var showBdc = this.getSessionStorageItem("showbreadcrumb") + //console.log("showBdc is: "+showBdc) + if(showBdc != undefined && showBdc=="false"){ + console.log("Show breadcrumb false"); + this.breadcrumbShow(false); + }else{ + console.log("Show breadcrumb true"); + this.breadcrumbShow(true); + } + }, + + + setSessionStorageItem : function (item_key, item_value) { + + // Check browser support + if (typeof(Storage) !== "undefined") { + // Store + sessionStorage.setItem(item_key, item_value); + return true; + } else { + console.log("Sorry, your browser does not support Web Storage..."); + return false; + } + }, + + + getSessionStorageItem : function (item_key) { + + // Check browser support + if (typeof(Storage) !== "undefined") { + // Store + return sessionStorage.getItem(item_key); + } else { + console.log("Sorry, your browser does not support Web Storage..."); + return undefined; + } + } + +} + + +CKAN_D4S_Functions_Util = { + + getPosition : function(canvas, event){ + var x = new Number(); + var y = new Number(); + try { + if (event.clientX != undefined && event.clientY != undefined) + { + + x = event.clientX; + y = event.clientY; + } + else // Firefox method to get the position + { + x = event.clientX + document.body.scrollLeft + + document.documentElement.scrollLeft; + y = event.clientY + document.body.scrollTop + + document.documentElement.scrollTop; + } + x -= canvas.offsetLeft; + y -= canvas.offsetTop; + }catch (err) { + //silent error + } + return '{"posX": "'+x+'", "posY": "'+y+'"}'; + }, + + // When the user clicks on div, open the popup + showPopupD4S : function(event, my_div, my_popup_left_position) { + var popup = document.getElementById(my_div); + var clickPosition = this.getPosition(my_div, event) + var myPosition = JSON.parse(clickPosition); + this.closePopups(my_div); + // When the user clicks anywhere outside of the modal, close it + /*window.onclick = function(event) { + if (event.target != popup) { + popup.style.display = "none"; + } + }*/ + popup.classList.toggle("show"); + + if(my_popup_left_position){ + popup.style.left = my_popup_left_position; + } + else if (myPosition.posX){ + popup.style.left = myPosition.posX + "px"; + } + }, + + closePopups : function ($target) { + var popups = document.getElementsByClassName('popuptext'); + for (i = 0; i < popups.length; i++) { + if (popups[i].getAttribute('id') != $target) { + popups[i].classList.remove('show'); + } + } + }, + + checkURL : function (url) { + //console.log('checking url: '+url) + var regex = new RegExp('^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/|www\.|ftp:\/\/)+[^ "]+$'); + if (regex.test(url)) { + return true; + } + return false; + } + +} + +CKAN_D4S_HTMLMessage_Util = { + +postHeightToPortlet : function (selectedProduct, product) { + var h = document.body.scrollHeight + "px"; + var p = ""; + var msg = ""; + //WORK AROUND IF TWO MESSAGES ARE SENT FROM A PAGE OF A PRODUCT + //THE MESSAGE WITH 'NULL' PRODUCT IS SKIPPED + //console.log("window.location.pathname? "+window.location.pathname); + var pathArray = window.location.pathname.split('/'); + var productContext = "dataset"; + if(pathArray.length>1){ + //console.log("pathArray is: "+pathArray); + var secondLevelLocation = pathArray[1]; //it is the second level location + //console.log("secondLevelLocation is: "+secondLevelLocation); + //console.log("h is: "+h); + if(secondLevelLocation == productContext){ //is it product context? + if(product !== 'undefined' && product !== null){ + p = product; + //console.log("product selected is: "+p); + }else{ + //console.log("product is null or undefined, passing only height"); + msg = "{\"height\": \""+h+"\"}"; + //window.postMessage(msg,'*'); + this.postMessageToParentWindow(msg); + return; + } + } + } + + //msg = "{'height': '"+h+"', 'product': '"+p+"'}"; + msg = "{\"height\": \""+h+"\", \"product\": \""+p+"\"}"; + //window.postMessage(msg,'*'); + //console.log("posting message in the window: "+msg); + this.postMessageToParentWindow(msg); + }, + + postMessageToParentWindow : function (msg) { + //window.postMessage(msg,'*'); + //console.log("posting message in the window: "+msg); + if(window.parent!=null){ + console.log("posting message in the parent window: "+msg); + window.parent.postMessage(msg,'*'); + } + return; + } + +} + +CKAN_D4S_JSON_Util = { + + +//ADDED by Francesco Mangiacrapa +appendHTMLToElement : function(containerID, elementHTML){ + + var divContainer = document.getElementById(containerID); + divContainer.innerHTML = ""; + divContainer.appendChild(elementHTML); +}, + +//ADDED by Francesco Mangiacrapa +jsonToHTML : function(containerID, cssClassToTable) { + + try + { + var jsonTxt = document.getElementById(containerID).innerHTML; + var jsonObj = JSON.parse(jsonTxt); + //console.log(jsonObj.length) + + if(jsonObj.length==undefined) + jsonObj = [jsonObj] + //console.log(jsonObj.length) + + // EXTRACT VALUE FOR HTML HEADER. + var col = []; + for (var i = 0; i < jsonObj.length; i++) { + for (var key in jsonObj[i]) { + //console.log('key json' +key) + if (col.indexOf(key) === -1) { + col.push(key); + } + } + } + + // CREATE DYNAMIC TABLE. + var table = document.createElement("table"); + var addDefaultCss = "json-to-html-table-column"; + if(cssClassToTable){ + addDefaultCss = cssClassToTable; + } + try{ + table.classList.add(addDefaultCss); + }catch(e){ + console.log('invalid css add', e); + } + + // ADD JSON DATA TO THE TABLE AS ROWS. + for (var i = 0; i < col.length; i++) { + + tr = table.insertRow(-1); + var firstCell = tr.insertCell(-1); + //firstCell.style.cssText="font-weight: bold; text-align: center; vertical-align: middle;"; + firstCell.innerHTML = col[i]; + for (var j = 0; j < jsonObj.length; j++) { + var tabCell = tr.insertCell(-1); + var theValue = jsonObj[j][col[i]]; + /* console.log(theValue + ' is url? '+isUrl);*/ + if(CKAN_D4S_Functions_Util.checkURL(theValue)){ + theValue = ''+theValue+''; + } + + tabCell.innerHTML = theValue; + } + } + + // FINALLY ADD THE NEWLY CREATED TABLE WITH JSON DATA TO A CONTAINER. + this.appendHTMLToElement(containerID, table); + + } + catch(e){ + console.log('invalid json', e); + } +} +} + + +//Task #8032 +window.addEventListener("message", +function (e) { + + var curr_loc = window.location.toString() + var orgin = e.origin.toString() + if(curr_loc.startsWith(orgin)){ + //alert("ignoring message from myself"); + return; + } + //console.log("origin: "+e.data) + if(e.data == null) + return; + + var pMess = JSON.parse(e.data) + //console.log(pMess.explore_vres_landing_page) + window.linktogateway = pMess.explore_vres_landing_page; +},false); +//old script ends + \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/assets/webassets.yml b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/assets/webassets.yml new file mode 100644 index 0000000..5b697a1 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/assets/webassets.yml @@ -0,0 +1,15 @@ +d4science-js: + filter: rjsmin + output: ckanext-d4science_theme/ckanext/d4science_theme/assets/js/d4science_scripts.js + contents: + - js/d4science_scripts.js + extra: + preload: + - base/main + +d4science-css: + filter: cssrewrite + output: ckanext-d4science_theme/ckanext/d4science_theme/assets/css/d4science_theme.css + contents: + - css/d4science_theme.css + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/__init__.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/__init__.py new file mode 100644 index 0000000..68f2e01 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/__init__.py @@ -0,0 +1,4 @@ +#The __init__.py files are required to make Python +#treat the directories as containing packages; this is done to prevent directories with a common name, such as string, from unintentionally hiding valid modules that occur later on the module search path. +#See: https://docs.python.org/3/tutorial/modules.html#packages + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/home.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/home.py new file mode 100644 index 0000000..5192b52 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/home.py @@ -0,0 +1,82 @@ +import logging +# from ckan.controllers.home import HomeController +import ckan.plugins as p +from ckan.common import _, g, c +from collections import OrderedDict + +import ckan.lib.search as search +import ckan.model as model +import ckan.logic as logic +import ckan.lib.maintain as maintain +import ckan.lib.base as base +import ckan.lib.helpers as h + +from flask import render_template + + +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + +class d4SHomeController(): + + #Overriding controllers.HomeController.index method + def index(self): + try: + # package search + context = {'model': model, 'session': model.Session,'user': c.user, 'auth_user_obj': c.userobj} + + facets = OrderedDict() + + default_facet_titles = { + 'organization': _('Organizations'), + 'groups': _('Groups'), + 'tags': _('Tags'), + 'res_format': _('Formats'), + 'license_id': _('Licenses'), + } + + for facet in g.facets: + if facet in default_facet_titles: + facets[facet] = default_facet_titles[facet] + else: + facets[facet] = facet + + # Facet titles + for plugin in p.PluginImplementations(p.IFacets): + facets = plugin.dataset_facets(facets, 'dataset') + + c.facet_titles = facets + + data_dict = { + 'q': '*:*', + 'facet.field': list(facets.keys()), + 'rows': 4, + 'start': 0, + 'sort': 'views_recent desc', + 'fq': 'capacity:"public"' + } + query = logic.get_action('package_search')(context, data_dict) + c.search_facets = query['search_facets'] + c.package_count = query['count'] + c.datasets = query['results'] + + #print "c.search_facets: " + #print " ".join(c.search_facets) + + except search.SearchError: + c.package_count = 0 + + if c.userobj and not c.userobj.email: + url = h.url_for(controller='user', action='edit') + msg = _('Please update your profile' + ' and add your email address. ') % url + \ + _('%s uses your email address' + ' if you need to reset your password.') \ + % g.site_title + h.flash_notice(msg, allow_html=True) + + #return base.render('home/index.html', cache_force=True) + #ckan 2.10 : + return render_template('home/index.html') + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/organization.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/organization.py new file mode 100644 index 0000000..90b85cc --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/organization.py @@ -0,0 +1,134 @@ +# encoding: utf-8 + +import re + +#import ckan.controllers.group as group +import ckan.plugins as plugins +import logging +import datetime +from urllib.parse import urlencode + +import ckan.plugins.toolkit as toolkit +import ckan.lib.base as base +import ckan.lib.helpers as h +import ckan.lib.maintain as maintain +import ckan.lib.navl.dictization_functions as dict_fns +import ckan.logic as logic +import ckan.lib.search as search +import ckan.model as model +import ckan.authz as authz +import ckan.lib.plugins +import ckan.plugins as plugins +from ckan.common import c, request, _ + + +''' +Created by Francesco Mangiacrapa, see: #8964 +''' +class OrganizationVREController(plugins.toolkit.DefaultOrganizationForm): #changed for 2.10 : GroupController -> defaultOrganizationForm + ''' The organization controller is for Organizations, which are implemented + as Groups with is_organization=True and group_type='organization'. It works + the same as the group controller apart from: + * templates and logic action/auth functions are sometimes customized + (switched using _replace_group_org) + * 'bulk_process' action only works for organizations + + Nearly all the code for both is in the GroupController (for historical + reasons). + ''' + + group_types = ['organization'] + + def _guess_group_type(self, expecting_name=False): + return 'organization' + + def _replace_group_org(self, string): + ''' substitute organization for group if this is an org''' + return re.sub('^group', 'organization', string) + + def _update_facet_titles(self, facets, group_type): + for plugin in plugins.PluginImplementations(plugins.IFacets): + facets = plugin.organization_facets( + facets, group_type, None) + + def index(self): + group_type = self._guess_group_type() + + page = h.get_page_number(request.args) or 1 + items_per_page = 21 + + context = {'model': model, 'session': model.Session, + 'user': c.user, 'for_view': True, + 'with_private': False} + + q = c.q = request.args.get('q', '') + sort_by = c.sort_by_selected = request.args.get('sort') + try: + logic.check_access('site_read', context) + logic.check_access('group_list', context) + except ckan.plugins.toolkit.NotAuthorized: + abort(403, _('Not authorized to see this page')) + + # pass user info to context as needed to view private datasets of + # orgs correctly + if c.userobj: + context['user_id'] = c.userobj.id + context['user_is_admin'] = c.userobj.sysadmin + + data_dict_global_results = { + 'all_fields': False, + 'q': q, + 'sort': sort_by, + 'type': group_type or 'group', + } + global_results = toolkit.get_action('group_list')(context, + data_dict_global_results) + + data_dict_page_results = { + 'all_fields': True, + 'q': q, + 'sort': sort_by, + 'type': group_type or 'group', + 'limit': items_per_page, + 'offset': items_per_page * (page - 1), + } + page_results = toolkit.get_action('group_list')(context, + data_dict_page_results) + + c.page = h.Page( + collection=global_results, + page=page, + url=h.pager_url, + items_per_page=items_per_page, + ) + + c.page.items = page_results + return base.render('organization_vre/index.html', + extra_vars={'group_type': group_type}) + + + def read(self, id, limit=20): + group_type = self._ensure_controller_matches_group_type( + id.split('@')[0]) + + context = {'model': model, 'session': model.Session, + 'user': c.user, + 'schema': self._db_to_form_schema(group_type=group_type), + 'for_view': True} + data_dict = {'id': id, 'type': group_type} + + # unicode format (decoded from utf8) + c.q = request.args.get('q', '') + + try: + # Do not query for the group datasets when dictizing, as they will + # be ignored and get requested on the controller anyway + data_dict['include_datasets'] = False + c.group_dict = toolkit.get_action('group_show')(context, data_dict) + c.group = context['group'] + except (NotFound, NotAuthorized, ckan.logic.NotFound): + abort(404, _('Group not found')) + + self._read(id, limit, group_type) + return base.render('organization_vre/read.html', + extra_vars={'group_type': group_type}) \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/systemtype.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/systemtype.py new file mode 100644 index 0000000..37e5593 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/controllers/systemtype.py @@ -0,0 +1,95 @@ +import logging +import ckan.plugins as p +from ckan.common import _, g, c +import ckan.lib.search as search +import ckan.model as model +import ckan.logic as logic +import ckan.lib.maintain as maintain +import ckan.lib.base as base +import ckan.lib.helpers as h + +from urllib.parse import urlencode + + +import ckan.lib.base as base +import ckan.lib.helpers as h +import ckan.lib.maintain as maintain +import ckan.lib.navl.dictization_functions as dict_fns +import ckan.logic as logic +import ckan.lib.search as search +import ckan.model as model +import ckan.authz as authz +import ckan.lib.plugins +import ckan.plugins as plugins +from ckan.common import c, g, request, _ + +from collections import OrderedDict +from flask import render_template + + + +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + +class d4STypeController(): + + #Overriding controllers.HomeController.index method + def index(self): + try: + # package search + context = {'model': model, 'session': model.Session,'user': c.user, 'auth_user_obj': c.userobj} + + facets = OrderedDict() + + default_facet_titles = { + 'organization': _('Organizations'), + 'groups': _('Groups'), + 'tags': _('Tags'), + 'res_format': _('Formats'), + 'license_id': _('Licenses'), + } + + for facet in g.facets: + if facet in default_facet_titles: + facets[facet] = default_facet_titles[facet] + else: + facets[facet] = facet + + # Facet titles + for plugin in p.PluginImplementations(p.IFacets): + facets = plugin.dataset_facets(facets, 'dataset') + + c.facet_titles = facets + + data_dict = { + 'q': '*:*', + 'facet.field': list(facets.keys()), + 'rows': 4, + 'start': 0, + 'sort': 'views_recent desc', + 'fq': 'capacity:"public"' + } + query = logic.get_action('package_search')(context, data_dict) + c.search_facets = query['search_facets'] + c.package_count = query['count'] + c.datasets = query['results'] + + #print "c.search_facets: " + #print " ".join(c.search_facets) + + except search.SearchError: + c.package_count = 0 + + if c.userobj and not c.userobj.email: + url = h.url_for(controller='user', action='edit') + msg = _('Please update your profile' + ' and add your email address. ') % url + \ + _('%s uses your email address' + ' if you need to reset your password.') \ + % g.site_title + h.flash_notice(msg, allow_html=True) + + # return base.render('type/index.html', cache_force=True) + return render_template('type/index.html') + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/__init__.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_cache_controller.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_cache_controller.py new file mode 100644 index 0000000..0462d7d --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_cache_controller.py @@ -0,0 +1,106 @@ +import datetime +import logging +import os +import tempfile +import csv + +from .icproxycontroller import NAMESPACE_ID_LABEL + +log = logging.getLogger(__name__) + +CATALINA_HOME = 'CATALINA_HOME' +temp_dir = None +namespaces_dir = None +NAMESPACES_DIR_NAME = "namespaces_for_catalogue" +NAMESPACES_CACHE_FILENAME = "Namespaces_Catalogue_Categories.csv" + +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + + +# D4S_Cache_Controller +class D4S_Cache_Controller(): + namespaces_cache_path = None + __scheduler = None + + def __init__(self): + """ Virtually private constructor. """ + log.debug("__init__ D4S_Cache_Controller") + self._check_cache() + + def _check_cache(self): + + if self.namespaces_cache_path is None: + self.init_temp_dir() + self.namespaces_cache_path = os.path.join(namespaces_dir, NAMESPACES_CACHE_FILENAME) + log.info("The namespaces cache is located at: %s" % self.namespaces_cache_path) + + if not os.path.exists(self.namespaces_cache_path): + log.info("File does not exists creating it") + try: + with open(self.namespaces_cache_path, mode='w') as namespaces_file: + csv.writer(namespaces_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + log.info("Cache created at %s" % self.namespaces_cache_path) + except Exception as e: + print(e) + + ''' Write the list of dictionary with namespaces''' + def write_namespaces(self, namespace_list_of_dict): + # Insert Data + with open(self.namespaces_cache_path, 'w') as namespaces_file: + writer = csv.writer(namespaces_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL) + writer.writerow([NAMESPACE_ID_LABEL, 'name', 'title', 'description']) + for namespace_dict in namespace_list_of_dict: + #print("namespace %s" % namespace_dict) + writer.writerow([namespace_dict[NAMESPACE_ID_LABEL], namespace_dict['name'], namespace_dict['title'], namespace_dict['description']]) + + log.info("Inserted %d namespaces in the Cache" % len(namespace_list_of_dict)) + + '''Returns the list of dictionary with namespaces''' + def read_namespaces(self): + # Read Data + namespace_list_of_dict = [] + try: + with open(self.namespaces_cache_path, 'r') as namespaces_file: + reader = csv.DictReader(namespaces_file) + for row in reader: + #print("read namespace %s" % row) + namespace_list_of_dict.append(dict(row)) + + log.debug("from Cache returning namespace_list_of_dict %s: " % namespace_list_of_dict) + log.info("from Cache read namespace_list_of_dict with %d item/s " % len(namespace_list_of_dict)) + return namespace_list_of_dict + except Exception as e: + print(e) + + log.info("no namespace in the Cache returning empty list of dict") + return namespace_list_of_dict + + @property + def get_namespaces_cache_path(self): + return self.namespaces_cache_path + + @classmethod + def init_temp_dir(cls): + global temp_dir + global NAMESPACES_DIR_NAME + global namespaces_dir + try: + temp_dir = str(os.environ[CATALINA_HOME]) + temp_dir = os.path.join(temp_dir, "temp") + except KeyError as error: + log.error("No environment variable for: %s" % CATALINA_HOME) + + if temp_dir is None: + temp_dir = tempfile.gettempdir() # using system tmp dir + + log.debug("Temp dir is: %s" % temp_dir) + + namespaces_dir = os.path.join(temp_dir, NAMESPACES_DIR_NAME) + + if not os.path.exists(namespaces_dir): + os.makedirs(namespaces_dir) + + + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_extras.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_extras.py new file mode 100644 index 0000000..8a62764 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_extras.py @@ -0,0 +1,30 @@ +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + +import logging +log = logging.getLogger(__name__) + +class D4S_Extras(): + + def __init__(self, category_dict={}, extras=[]): + self._category = category_dict + self._extras = extras + + def append_extra(self, k, v): + #print ("self._extras: %s" %self._extras) + if k is not None: + self._extras.append({k:v}) + + @property + def category(self): + return self._category + + @property + def extras(self): + return self._extras + + def __repr__(self): + return 'category: %s'%self._category+' ' \ + 'extras: %s'%self._extras + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_namespaces.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_namespaces.py new file mode 100644 index 0000000..54f1e90 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_namespaces.py @@ -0,0 +1,39 @@ +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + +#OrderedDict([(u'@id', u'extra_information'), (u'name', u'Extra Information'), (u'title', u'Extras'), (u'description', u'This section is about Extra(s)')]), u'contact': OrderedDict([(u'@id', u'contact'), (u'name', u'Contact'), (u'title', u'Contact Title'), (u'description', u'This section is about Contact(s)')]), u'developer_information': OrderedDict([(u'@id', u'developer_information'), (u'name', u'Developer'), (u'title', u'Developer Information'), (u'description', u'This section is about Developer(s)')])} + +import logging +log = logging.getLogger(__name__) + +class D4S_Namespaces(): + + def __init__(self, id=None, name=None, title=None, description=None): + self._id = id + self._name = name + self._title = title + self._description = description + + @property + def id(self): + return self._id + + @property + def name(self): + return self._name + + @property + def title(self): + return self._title + + + @property + def description(self): + return self._description + + def __repr__(self): + return '{id: %s'%self.id+', ' \ + 'name: %s'%self.name+ ', ' \ + 'title: %s'%self.title+ ', ' \ + 'description: %s'%self.description+ '}' diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_namespaces_controller.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_namespaces_controller.py new file mode 100644 index 0000000..afb88df --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_namespaces_controller.py @@ -0,0 +1,130 @@ +import logging +import time + +from .d4s_cache_controller import D4S_Cache_Controller +from .icproxycontroller import D4S_IS_DiscoveryCatalogueNamespaces +from threading import Event, Thread + +CATEGORY = 'category' +NOCATEOGORY = 'nocategory' + +log = logging.getLogger(__name__) + +cancel_future_calls = None + +# Refreshing time for namespaces cache in secs. +NAMESPACES_CACHE_REFRESHING_TIME = 60 * 60 + + +# Funtion to call repeatedly another function +def call_repeatedly(interval, func, *args): + log.info("call_repeatedly called on func '{}' with interval {} sec".format(func.__name__, interval)) + stopped = Event() + + def loop(): + while not stopped.wait(interval): # the first call is in `interval` secs + func(*args) + + th = Thread(name='daemon_caching_namespaces', target=loop) + th.setDaemon(True) + th.start() + return stopped.set + + +def reload_namespaces_from_IS(urlICProxy, resourceID, gcubeToken): + log.info("_reload_namespaces_from_IS called") + try: + discovery_ctg_namespaces = D4S_IS_DiscoveryCatalogueNamespaces(urlICProxy, resourceID, gcubeToken) + namespaces_list_of_dict = discovery_ctg_namespaces.getNamespacesDictFromResource() + + if namespaces_list_of_dict is not None and len(namespaces_list_of_dict) > 0: + log.debug("namespaces read from IS are: %s" % namespaces_list_of_dict) + D4S_Cache_Controller().write_namespaces(namespaces_list_of_dict) + else: + log.info("namespaces list read from IS is empty. Skipping caching update") + + except Exception as e: + print("Error occurred on reading namespaces from IS and refilling the cache!") + print(e) + + +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + + +# D4S_IS_DiscoveryCatalogueNamespacesController is used to discovery namespaces for Catalogue Categories (implemented as a Singleton) +# @param: urlICProxy is the URI of IC proxy rest-full service provided by IS +# @param: resourceID is the resource ID of the Generic Resource: "Namespaces Catalogue Categories" +# @param: gcubeToken the gcube token used to contact the IC proxy +class D4S_Namespaces_Controller(): + __instance = None + + @staticmethod + def getInstance(): + """ Static access method. """ + if D4S_Namespaces_Controller.__instance is None: + D4S_Namespaces_Controller() + + return D4S_Namespaces_Controller.__instance + + def __init__(self): + """ Virtually private constructor. """ + log.debug("__init__ D4S_Namespaces_Controller") + + if D4S_Namespaces_Controller.__instance is not None: + raise Exception("This class is a singleton!") + else: + D4S_Namespaces_Controller.__instance = self + + self._d4s_cache_controller = D4S_Cache_Controller() + self._urlICProxy = None + self._resourceID = None + self._gcubeToken = None + + def load_namespaces(self, urlICProxy, resourceID, gcubeToken): + log.debug("readNamespaces called") + self._urlICProxy = urlICProxy + self._resourceID = resourceID + self._gcubeToken = gcubeToken + return self._check_namespaces() + + def _read_namespaces(self): + return self._d4s_cache_controller.read_namespaces() + + def _check_namespaces(self): + log.debug("_check_namespaces called") + + if self._d4s_cache_controller is None: + self._d4s_cache_controller = D4S_Cache_Controller() + + namespace_list = self._read_namespaces() + + # when the Cache is empty + if namespace_list is None or not namespace_list: + # reading namespaces from IS and filling the DB + log.info("The Cache is empty. Reading the namespace from IS and filling the Cache") + reload_namespaces_from_IS(self._urlICProxy, self._resourceID, self._gcubeToken) + # reloading the namespaces from the cache + namespace_list = self._read_namespaces() + + # starting Thread daemon for refreshing the namespaces Cache + global cancel_future_calls + if cancel_future_calls is None: + cancel_future_calls = call_repeatedly(NAMESPACES_CACHE_REFRESHING_TIME, reload_namespaces_from_IS, + self._urlICProxy, + self._resourceID, + self._gcubeToken) + + return namespace_list + + def get_dict_ctg_namespaces(self): + log.debug("get_dict_ctg_namespaces called") + namespace_list_of_dict = self._check_namespaces() + return self.convert_namespaces_to_d4s_namespacedict(namespace_list_of_dict) + + # Private method + @staticmethod + def convert_namespaces_to_d4s_namespacedict(namespace_list_of_dict): + log.debug("convert_namespaces_to_d4s_namespacedict called on %s" % namespace_list_of_dict) + return D4S_IS_DiscoveryCatalogueNamespaces.to_namespaces_dict_index_for_id(namespace_list_of_dict) diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_namespaces_extras_util.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_namespaces_extras_util.py new file mode 100644 index 0000000..edb0bbf --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/d4s_namespaces_extras_util.py @@ -0,0 +1,89 @@ +import logging +import collections +from .d4s_extras import D4S_Extras + +CATEGORY = 'category' +NOCATEOGORY = 'nocategory' + +log = logging.getLogger(__name__) + +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + + +# D4S_Namespaces_Extra_Util is used to get the extra fields indexed for D4Science namespaces +# @param: namespace_dict is the namespace dict of D4Science namespaces (defined in the Generic Resource: "Namespaces Catalogue Categories") +# @param: extras is the dictionary of extra fields for a certain item +class D4S_Namespaces_Extra_Util(): + + def get_extras_indexed_for_namespaces(self, namespace_dict, extras): + extras_for_categories = collections.OrderedDict() + + # ADDING ALL EXTRAS WITH NAMESPACE + for namespaceid in list(namespace_dict.keys()): + dict_extras = None + nms = namespaceid + ":" + #has_namespace_ref = None + for key, value in extras: + k = key + v = value + # print "key: " + k + # print "value: " + v + if k.startswith(nms): + + if namespaceid not in extras_for_categories: + extras_for_categories[namespaceid] = collections.OrderedDict() + + dict_extras = extras_for_categories[namespaceid] + log.debug("dict_extras %s "%dict_extras) + + if (dict_extras is None) or (not dict_extras): + dict_extras = D4S_Extras(namespace_dict.get(namespaceid), []) + log.debug("dict_extras after init %s " % dict_extras) + + #print ("dict_extras after init %s " % dict_extras) + log.debug("replacing namespace into key %s " % k +" with empty string") + nms = namespaceid + ":" + k = k.replace(nms, "") + dict_extras.append_extra(k, v) + extras_for_categories[namespaceid] = dict_extras + log.debug("adding d4s_extra: %s " % dict_extras+ " - to namespace id: %s" %namespaceid) + #has_namespace_ref = True + #break + + #ADDING ALL EXTRAS WITHOUT NAMESPACE + for key, value in extras: + k = key + v = value + + has_namespace_ref = None + for namespaceid in list(namespace_dict.keys()): + nms = namespaceid + ":" + #IF KEY NOT STARTING WITH NAMESPACE + if k.startswith(nms): + has_namespace_ref = True + log.debug("key: %s " % k + " - have namespace: %s" % nms) + break + + if has_namespace_ref is None: + log.debug("key: %s " % k + " - have not namespace") + if NOCATEOGORY not in extras_for_categories: + extras_for_categories[NOCATEOGORY] = collections.OrderedDict() + + dict_extras_no_cat = extras_for_categories[NOCATEOGORY] + #print ("dict_extras_no_cat %s " % dict_extras_no_cat) + + if (dict_extras_no_cat is None) or (not dict_extras_no_cat): + dict_extras_no_cat = D4S_Extras(NOCATEOGORY, []) + + #print ("adding key: %s "%k+" - value: %s"%v) + log.debug("NOCATEOGORY adding key: %s " % k + " - value: %s" % v) + + dict_extras_no_cat.append_extra(k, v) + log.debug("dict_extras_no_cat %s " % dict_extras_no_cat) + extras_for_categories[NOCATEOGORY] = dict_extras_no_cat + log.debug("extras_for_categories NOCATEOGORY %s " % extras_for_categories) + + return extras_for_categories + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/icproxycontroller.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/icproxycontroller.py new file mode 100644 index 0000000..f1c03ce --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/d4sdiscovery/icproxycontroller.py @@ -0,0 +1,110 @@ +import logging +import urllib.request, urllib.error, urllib.parse +from lxml import etree + +import xmltodict +import collections + +from .d4s_namespaces import D4S_Namespaces + +XPATH_NAMESPACES = "/Resource/Profile/Body/namespaces" +gcubeTokenParam = "gcube-token" +NAMESPACE_ID_LABEL = '@id' + +log = logging.getLogger(__name__) + + +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + +def getResponseBody(uri): + req = urllib.request.Request(uri) + try: + resp = urllib.request.urlopen(req, timeout=20) + except urllib.error.HTTPError as e: + log.error("Error on contacting URI: %s" % uri) + log.error("HTTPError: %d" % e.code) + return None + except urllib.error.URLError as e: + # Not an HTTP-specific error (e.g. connection refused) + log.error("URLError - Input URI: %s " % uri + " is not valid!!") + return None + else: + # 200 + body = resp.read() + return body + + +# D4S_IS_DiscoveryCatalogueNamespaces is used to discovery namespaces for Catalogue Categories. +# @param: urlICProxy is the URI of IC proxy rest-full service provided by IS +# @param: resourceID is the resource ID of the Generic Resource: "Namespaces Catalogue Categories" +# @param: gcubeToken the gcube token used to contact the IC proxy +class D4S_IS_DiscoveryCatalogueNamespaces(): + + def __init__(self, urlICProxy, resourceID, gcubeToken): + self.urlICProxy = urlICProxy + self.resourceID = resourceID + self.gcubeToken = gcubeToken + + def getNamespacesDictFromResource(self): + + doc = {} + namespace_list = [] + + try: + # print("proxy: "+self.urlICProxy) + # print("resourceID: " + self.resourceID) + # print("gcubeTokenParam: " + gcubeTokenParam) + # print("gcubeToken: " + self.gcubeToken) + + uri = self.urlICProxy + "/" + self.resourceID + "?" + gcubeTokenParam + "=" + self.gcubeToken + log.info("Contacting URL: %s" % uri) + theResource = getResponseBody(uri) + log.debug("Resource returned %s " % theResource) + theResourceXML = etree.XML(theResource) + theNamespaces = theResourceXML.xpath(XPATH_NAMESPACES) + log.debug("The body %s" % etree.tostring(theNamespaces[0])) + + if theNamespaces is not None and theNamespaces[0] is not None: + bodyToString = etree.tostring(theNamespaces[0]) + doc = xmltodict.parse(bodyToString) + else: + log.warn("No Namespace for Catalogue Categories found, returning None") + except Exception as inst: + log.error("Error on getting catalogue namespaces: " + str(inst)) + log.info("Returning empty list of namespaces") + return namespace_list + + log.debug("IS namespaces resource to dict is: %s" % doc) + + + if ('namespaces' in doc): + # log.debug('Namespaces obj %s:' % doc['namespaces']) + namespaces = doc['namespaces'] + if doc is not None and 'namespace' in namespaces: + namespace_list = namespaces['namespace'] + + log.info("Loaded %d namespaces from IS resource" % len(namespace_list)) + return namespace_list + + @staticmethod + def to_namespaces_dict_index_for_id(namespace_list): + namespace_dict = collections.OrderedDict() + log.debug("namespaces to dict: %s" % namespace_list) + try: + if namespace_list is not None and len(namespace_list) > 0: + for namespace in namespace_list: + try: + if NAMESPACE_ID_LABEL in namespace: + namespace_dict[namespace[NAMESPACE_ID_LABEL]] = D4S_Namespaces( + namespace[NAMESPACE_ID_LABEL], + namespace['name'], + namespace['title'], + namespace['description']) + except Exception as inst: + log.error("Error on converting catalogue namespaces: " + str(inst)) + except Exception as inst: + log.error("Error on checking namespace_list: " + str(inst)) + # print "namespace_dict to Nam: %s"%namespace_dict + return namespace_dict diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/.gitignore b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/d4science_scripts.js b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/d4science_scripts.js new file mode 100644 index 0000000..642cdc5 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/d4science_scripts.js @@ -0,0 +1,303 @@ +/* ===================================================== + JavaScript used by CKAN plugin: 'd4science_theme' + Created by Francesco Mangiacrapa ISTI-CNR Pisa, Italy + ===================================================== */ + + +CKAN_D4S_Breadcrumb_Manager = { + + breadcrumbShow : function (show) { + + var breadcrumb = document.getElementById('ckan-breadcrumb'); + console.log('breadcrumb is '+breadcrumb) + if(breadcrumb){ + if(show){ + breadcrumb.style.display = 'block'; + this.organizationTreeShow(true); + } else{ + breadcrumb.style.display = 'none'; + this.organizationTreeShow(false); + } + } + + //var elements = document.getElementsByTagName('a'); + //for(var i = 0, len = elements.length; i < len; i++) { + // elements[i].onclick = function () { + // //alert("You clicked an external link to: " + this.href); + // //window.parent.add_hide_breadcrumb_to_dom(false); + // this.add_hide_breadcrumb_to_dom(false); + // } + //} + }, + + organizationTreeShow : function (show) { + var trees = document.getElementsByClassName("hierarchy-tree-top"); + + if (trees){ + for (i = 0; i < trees.length; i++) { + if(show){ + trees[i].style.display = 'block'; + } else{ + trees[i].style.display = 'none'; + } + } + } + }, + + checkBreadcrumbShow : function () { + + var showBdc = this.getSessionStorageItem("showbreadcrumb") + //console.log("showBdc is: "+showBdc) + if(showBdc != undefined && showBdc=="false"){ + console.log("Show breadcrumb false"); + this.breadcrumbShow(false); + }else{ + console.log("Show breadcrumb true"); + this.breadcrumbShow(true); + } + }, + + + setSessionStorageItem : function (item_key, item_value) { + + // Check browser support + if (typeof(Storage) !== "undefined") { + // Store + sessionStorage.setItem(item_key, item_value); + return true; + } else { + console.log("Sorry, your browser does not support Web Storage..."); + return false; + } + }, + + + getSessionStorageItem : function (item_key) { + + // Check browser support + if (typeof(Storage) !== "undefined") { + // Store + return sessionStorage.getItem(item_key); + } else { + console.log("Sorry, your browser does not support Web Storage..."); + return undefined; + } + } + +} + + +CKAN_D4S_Functions_Util = { + + getPosition : function(canvas, event){ + var x = new Number(); + var y = new Number(); + try { + if (event.clientX != undefined && event.clientY != undefined) + { + + x = event.clientX; + y = event.clientY; + } + else // Firefox method to get the position + { + x = event.clientX + document.body.scrollLeft + + document.documentElement.scrollLeft; + y = event.clientY + document.body.scrollTop + + document.documentElement.scrollTop; + } + x -= canvas.offsetLeft; + y -= canvas.offsetTop; + }catch (err) { + //silent error + } + return '{"posX": "'+x+'", "posY": "'+y+'"}'; + }, + + // When the user clicks on div, open the popup + showPopupD4S : function(event, my_div, my_popup_left_position) { + var popup = document.getElementById(my_div); + var clickPosition = this.getPosition(my_div, event) + var myPosition = JSON.parse(clickPosition); + this.closePopups(my_div); + // When the user clicks anywhere outside of the modal, close it + /*window.onclick = function(event) { + if (event.target != popup) { + popup.style.display = "none"; + } + }*/ + popup.classList.toggle("show"); + + if(my_popup_left_position){ + popup.style.left = my_popup_left_position; + } + else if (myPosition.posX){ + popup.style.left = myPosition.posX + "px"; + } + }, + + closePopups : function ($target) { + var popups = document.getElementsByClassName('popuptext'); + for (i = 0; i < popups.length; i++) { + if (popups[i].getAttribute('id') != $target) { + popups[i].classList.remove('show'); + } + } + }, + + checkURL : function (url) { + //console.log('checking url: '+url) + var regex = new RegExp('^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/|www\.|ftp:\/\/)+[^ "]+$'); + if (regex.test(url)) { + return true; + } + return false; + } + +} + +CKAN_D4S_HTMLMessage_Util = { + + postHeightToPortlet : function (selectedProduct, product) { + var h = document.body.scrollHeight + "px"; + var p = ""; + var msg = ""; + //WORK AROUND IF TWO MESSAGES ARE SENT FROM A PAGE OF A PRODUCT + //THE MESSAGE WITH 'NULL' PRODUCT IS SKIPPED + //console.log("window.location.pathname? "+window.location.pathname); + var pathArray = window.location.pathname.split('/'); + var productContext = "dataset"; + if(pathArray.length>1){ + //console.log("pathArray is: "+pathArray); + var secondLevelLocation = pathArray[1]; //it is the second level location + //console.log("secondLevelLocation is: "+secondLevelLocation); + //console.log("h is: "+h); + if(secondLevelLocation == productContext){ //is it product context? + if(product !== 'undefined' && product !== null){ + p = product; + //console.log("product selected is: "+p); + }else{ + //console.log("product is null or undefined, passing only height"); + msg = "{\"height\": \""+h+"\"}"; + //window.postMessage(msg,'*'); + this.postMessageToParentWindow(msg); + return; + } + } + } + + //msg = "{'height': '"+h+"', 'product': '"+p+"'}"; + msg = "{\"height\": \""+h+"\", \"product\": \""+p+"\"}"; + //window.postMessage(msg,'*'); + //console.log("posting message in the window: "+msg); + this.postMessageToParentWindow(msg); + }, + + postMessageToParentWindow : function (msg) { + //window.postMessage(msg,'*'); + //console.log("posting message in the window: "+msg); + if(window.parent!=null){ + console.log("posting message in the parent window: "+msg); + window.parent.postMessage(msg,'*'); + } + return; + } + +} + +CKAN_D4S_JSON_Util = { + + + //ADDED by Francesco Mangiacrapa + appendHTMLToElement : function(containerID, elementHTML){ + + var divContainer = document.getElementById(containerID); + divContainer.innerHTML = ""; + divContainer.appendChild(elementHTML); + }, + + //ADDED by Francesco Mangiacrapa + jsonToHTML : function(containerID, cssClassToTable) { + + try + { + var jsonTxt = document.getElementById(containerID).innerHTML; + var jsonObj = JSON.parse(jsonTxt); + //console.log(jsonObj.length) + + if(jsonObj.length==undefined) + jsonObj = [jsonObj] + //console.log(jsonObj.length) + + // EXTRACT VALUE FOR HTML HEADER. + var col = []; + for (var i = 0; i < jsonObj.length; i++) { + for (var key in jsonObj[i]) { + //console.log('key json' +key) + if (col.indexOf(key) === -1) { + col.push(key); + } + } + } + + // CREATE DYNAMIC TABLE. + var table = document.createElement("table"); + var addDefaultCss = "json-to-html-table-column"; + if(cssClassToTable){ + addDefaultCss = cssClassToTable; + } + try{ + table.classList.add(addDefaultCss); + }catch(e){ + console.log('invalid css add', e); + } + + // ADD JSON DATA TO THE TABLE AS ROWS. + for (var i = 0; i < col.length; i++) { + + tr = table.insertRow(-1); + var firstCell = tr.insertCell(-1); + //firstCell.style.cssText="font-weight: bold; text-align: center; vertical-align: middle;"; + firstCell.innerHTML = col[i]; + for (var j = 0; j < jsonObj.length; j++) { + var tabCell = tr.insertCell(-1); + var theValue = jsonObj[j][col[i]]; + /* console.log(theValue + ' is url? '+isUrl);*/ + if(CKAN_D4S_Functions_Util.checkURL(theValue)){ + theValue = ''+theValue+''; + } + + tabCell.innerHTML = theValue; + } + } + + // FINALLY ADD THE NEWLY CREATED TABLE WITH JSON DATA TO A CONTAINER. + this.appendHTMLToElement(containerID, table); + + } + catch(e){ + console.log('invalid json', e); + } + } +} + + +//Task #8032 +window.addEventListener("message", + function (e) { + + var curr_loc = window.location.toString() + var orgin = e.origin.toString() + if(curr_loc.startsWith(orgin)){ + //alert("ignoring message from myself"); + return; + } + //console.log("origin: "+e.data) + if(e.data == null) + return; + + var pMess = JSON.parse(e.data) + //console.log(pMess.explore_vres_landing_page) + window.linktogateway = pMess.explore_vres_landing_page; + },false); + \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/d4science_theme.css b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/d4science_theme.css new file mode 100644 index 0000000..9e416b1 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/fanstatic/d4science_theme.css @@ -0,0 +1,844 @@ +/* ===================================================== + The "account masthead" bar across the top of the site + ===================================================== */ + +.account-masthead { + background-color: #ccc; +} +/* The "bubble" containing the number of new notifications. */ +.account-masthead .account .notifications a span { + background-color: #9fa0a2; +} +/* The text and icons in the user account info. */ +.account-masthead .account ul li a { + color: rgba(255, 255, 255, 0.6); +} +/* The user account info text and icons, when the user's pointer is hovering + over them. */ +.account-masthead .account ul li a:hover { + color: rgba(255, 255, 255, 0.7); +/* background-color: black;*/ +} + + +/* ======================================================================== + The main masthead bar that contains the site logo, nav links, and search + ======================================================================== */ + +.masthead { + background: #eee url("/bg-noise.png") repeat scroll 0 0; + border-top: 1px solid #555; + padding-top: 5px; + padding-bottom: 15px !important; + border-bottom: 1px solid #999; +/* background-image: url("/bg-pattern.min.svg") !important; */ +} + +.masthead .navigation .nav-pills li a{ + color: #187794; +} + +a.logo > img{ + margin-bottom: 5px; +} + +/* The "navigation pills" in the masthead (the links to Datasets, + Organizations, etc) when the user's pointer hovers over them. */ +.masthead .navigation .nav-pills li a:hover { +/* background-color: rgb(48, 48, 48);*/ + color: white; +} +/* The "active" navigation pill (for example, when you're on the /dataset page + the "Datasets" link is active). */ +.masthead .navigation .nav-pills li.active a { + background-color: #d2d2d5; +} +/* The "box shadow" effect that appears around the search box when it + has the keyboard cursor's focus. */ +.masthead input[type="text"]:focus { + -webkit-box-shadow: inset 0px 0px 2px 0px rgba(0, 0, 0, 0.7); + box-shadow: inset 0px 0px 2px 0px rgba(0, 0, 0, 0.7); +} + + +/* =========================================== + The content in the middle of the front page + =========================================== */ + +/* Remove the "box shadow" effect around various boxes on the page. */ +.box { + box-shadow: none; +} +.hero { + background: #FEFEFE repeat scroll 0 0 !important; +} +/* Remove the borders around the "Welcome to CKAN" and "Search Your Data" + boxes. */ +.hero .box { + /*border: none;*/ + margin-top: 10px !important; +} +/* Change the colors of the "Search Your Data" box. */ +.homepage .module-search .module-content { + color: rgb(68, 68, 68); + background-color: white; +} +/* Change the background color of the "Popular Tags" box. */ +.homepage .module-search .tags { + background-color: #fcfcfc; +} + +.homepage-title{ + font-size: 20px; + font-weight: bold; + color: #202020; + margin-bottom: 20px; +} + +/* Change the background color of the "Popular Tags" box. */ +.homepage .module-search h3{ + color: #444; +} + +/* Remove some padding. This makes the bottom edges of the "Welcome to CKAN" + and "Search Your Data" boxes line up. */ +.module-content:last-child { + /*padding-bottom: 0px;*/ +} +.homepage .module-search { + padding: 0px; +} +/* Add a border line between the top and bottom halves of the front page. */ +.homepage [role="main"] { + border-bottom: 1px solid #bbb; + padding: 10px 0; +} + +.homepage .stats ul li a b{ + font-size: 30px !important; +} + +[role="main"], .main { +/* background: #f5f6fa url("/bg-pattern.min.svg") repeat; scroll 0 0;*/ + /*background: #fafafa url("/bg-pattern.svg") repeat; scroll 0 0;*/ + background: #fdfdfd none repeat scroll 0 0; + min-height: 0px !important; +} + +.media-item-homepage { + background-color: white; + border-radius: 3px; + float: left; + margin: 15px 0 0 15px; + overflow: hidden; + padding-left: 10px; + padding-right: 10px; + position: relative; + text-align: center; + width: 150px; +} + +.media-heading-homepage { + font-size: 16px; + hyphens: auto; + line-height: 1.3; + margin: 5px 0; +} + +.media-grid-homepage { + -moz-border-bottom-colors: none; + -moz-border-left-colors: none; + -moz-border-right-colors: none; + -moz-border-top-colors: none; +/* background: #fbfbfb url("../../../base/images/bg.png") repeat scroll 0 0; + border-color: #dddddd; + border-image: none; + border-style: solid; + border-width: 1px 0;*/ + list-style: outside none none; + margin: 0 -10px; + padding-bottom: 15px; +} +.media-grid-homepage::before, .media-grid::after { + content: ""; + display: table; + line-height: 0; +} +.media-grid-homepage::after { + clear: both; +} + +.background-circle{ + padding: 10px 10px; + display: inline-block !important; + -webkit-border-radius: 90px; + -moz-border-radius: 90px; + border-radius: 90px; + background-color: #4679b2; + text-decoration: none !important; +} + +.color-white{ + color: white !important; +} + +.badge-circle { + border-radius: 50% 50% 50% 50% !important; + height: 60px; + text-align: center; + vertical-align: middle; + width: 65px; + background-color: #4679b2; + display: inline-block !important; + padding-top: 5px; + text-decoration: none !important; +} + +/* ==================================== + The footer at the bottom of the site + ==================================== */ + +.site-footer, body { + background-color: #bbb; + font-family: "Lato","Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 16px; +} +/* The text in the footer. */ +.site-footer, +.site-footer label, +.site-footer small { + color: rgba(255, 255, 255, 0.6); +} +/* The link texts in the footer. */ +.site-footer a { + color: rgba(255, 255, 255, 0.6); +} + +.site-footer-internal{ + min-height: 10px; + padding: 2px 0; + font-size: 12px; +} + +.site-footer-internal { + /*background-color: rgba(255, 255, 255, 0.6);*/ + text-align: center; + /*display: inline-block;*/ +} + +.site-footer-internal, +.site-footer-internal label, +.site-footer-internal small { + +} + +.site-footer-internal a { + display: inline-block; +} + +.d4s-hide-text { + background-color: transparent; + border: 0 none; + color: transparent; + font: 0px/0 a; + text-shadow: none; +} + +.d4science-footer-logo { + background: url("/gCube_70.png") no-repeat scroll left top rgba(0, 0, 0, 0); + height: 32px; + margin-top: 2px; + text-indent: -900em; + width: 75px; +} + +.d4s-ckan-footer-logo { + background: rgba(0, 0, 0, 0) url("/ckan-logo-footer.png") no-repeat scroll left top; + height: 21px; + margin-top: 2px; + text-indent: -900em; + width: 69px; +} + +.site-footer-d4science { + font-size: 14px; + color: #f5f5f5; + text-align: center; + height: 25px; + padding-top: 5px; + background-color: #7F7F7F; +} + +.site-footer-d4science a { + font-weight: bold; + text-decoration: none; + color: white; +} + + +/* ==================================== + Base elements of the site + ==================================== */ + +div .principaltitle { + color: inherit; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 20px; + font-weight: bold; + line-height: 1.2; + margin: 15px 0; + text-rendering: optimizelegibility; + word-break: break-all; + padding-bottom: 10px; + padding-top: 5px; + border-bottom: 1px solid #eee; +} + +div .notes { + color: #444444; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 14px; + line-height: 1.3; + text-align: justify; + word-break: break-all; +} + +div .infotitle { + font-size: 15px; + hyphens: auto; + line-height: 1.3; + word-break: break-all; + font-weight: bold; +} + +.toolbar .breadcrumb{ + font-size: 16px !important; +} + +.box{ + border: 0px !important; +} + +div .sectiontitle{ + color: #9F9F9F; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 17px; + font-weight: bold; + margin: 20px 0; + margin-top: 20px; + margin-bottom: 10px; + text-rendering: optimizelegibility; +} + +section .well { + background-color: #fdfdfd !important; + border: 1px solid #e3e3e3; + border-radius: 4px; + box-shadow: none !important; + margin-bottom: 20px; + min-height: 20px; + +} + +.page-heading { + font-size: 18px; + line-height: 1.2; + margin-top: 20px; + margin-bottom: 0px; +} + +#dataset-resources .resource-list{ + background-color: #fdfdfd !important; + border: 1px solid #e3e3e3; + border-radius: 4px; + box-shadow: none !important; + margin: -1px 0 !important; +} + +.wrapper{ + border: 1px solid #d0d0d0; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05); + border-radius: 3px +} + +.home-popular{ + padding-top: 25px; +} + +.logo-homepage{ + max-height: 60px; +} + +.statistics-show{ + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + color: #444444; + text-decoration: none; +} + +.d4s-center-cropped{ + text-align: center; + background-color: #eee; + border: 1px solid #ddd; + padding-bottom: 10px; + padding-top: 10px; +} + +.tag-list { + font-size: 14px; +} + + +/* ==================================== + Acquired Dataset + ==================================== */ +.label-acquired { + background-color: #55a1ce; +} + +.label-owner { + background-color: #e0051e; +} + +.divider { + margin-left:10px; + height:auto; + display:inline-block; +} + +/* ==================================== + List Dataset + ==================================== */ + +/*LEFT +.show_meatadatatype { + color: white; + display: inline-block; // Inline elements with width and height. TL;DR they make the icon buttons stack from left-to-right instead of top-to-bottom + position: relative; // All 'absolute'ly positioned elements are relative to this one + margin-bottom: 20px; + margin-left: 25px; +} +*/ + +/*RIGHT*/ +.show_meatadatatype { + color: white; + display: inline-block; + float: right; + margin-right: 2px; + margin-top: -20px; + position: relative; +} + + + +/* LEFT + * Position the badge within the relatively positioned button +.button__badge { + background-color: #fa3e3e; + border-radius: 2px; + color: white; + + padding: 1px 6px; + font-size: 10px; + + position: absolute; + top: 0; + right: 0; +}*/ + + + +/* RIGTH */ +.button__badge { + color: #808080; + padding: 0px 2px; + font-size: 10px; + top: 0; + right: 0; + font-family: sans-serif, times, georgia; +} + +/* ==================================== + Modal Popup + ==================================== */ + +/* Popup container - can be anything you want */ +/* The Modal (background) */ +.d4s_modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 10001; /* Sit on top (NB. At 1000 there is the zoom in/out of the Map Widget)*/ + /*padding-top: 100px;*/ /* Location of the box */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ +} + +/* Modal Content */ +.d4s_modal-content { + background-color: #fefefe; + /*margin: auto;*/ + padding: 20px; + border: 1px solid #888; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + position: absolute; + left: 50%; + margin-left: -225px; + width: 450px; +} + +/* The Close Button */ +.d4s_close { + color: #aaaaaa; + float: right; + font-size: 28px; + font-weight: bold; + padding-left: 20px; +} + +.d4s_close:hover, +.d4s_close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +.d4s_div_clickable{ + cursor: pointer; +} + +/*==================================== +D4S POPUP +======================================*/ + +/* Popup container - can be anything you want */ +.popupD4SNoArrow { + position: relative; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* The actual popup */ +.popupD4SNoArrow .popuptext { + visibility: hidden; + width: 300px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 8px; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + margin-left: -150px; +} + +/* Toggle this class - hide and show the popup */ +.popupD4SNoArrow .show { + visibility: visible; + -webkit-animation: fadeIn 1s; + animation: fadeIn 1s; +} + + +/* Popup container - can be anything you want */ +.popupD4S { + position: relative; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* The actual popup */ +.popupD4S .popuptext { + visibility: hidden; + width: 300px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 8px; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + margin-left: -150px; +} + +/* Popup arrow */ +.popupD4S .popuptext::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #555 transparent transparent transparent; +} + +/* Toggle this class - hide and show the popup */ +.popupD4S .show { + visibility: visible; + -webkit-animation: fadeIn 1s; + animation: fadeIn 1s; +} + +/* Add animation (fade in the popup) */ +@-webkit-keyframes fadeIn { + from {opacity: 0;} + to {opacity: 1;} +} + +@keyframes fadeIn { + from {opacity: 0;} + to {opacity:1 ;} +} + +/*==================================== +D4S PACKAGE +======================================*/ + +.graphic-preview-style { + text-align: center; + border-top: 1px dotted #DDD; + padding-top: 10px; + padding-bottom: 0px; + margin-top: 15px; +} + +.graphic-preview-style a{ + font-size: 13px; +} + +.graphic-preview-style img{ + max-width: 100% !important; + height: auto; + +} + +.graphic-preview-style #graphic-title{ + font-size: 13px; + +} + +.nav-item{ + word-wrap:break-word; + } + +/*==================================== +RESOURCE_LIST RESOURCE_ITEM INTO PACKAGE +======================================*/ + +.required-access { + font-style: italic; + font-weight: bold; + padding: 5px; +} + +/*==================================== +LINK TO RESOURCES FROM PACKAGE LIST +======================================*/ + +.dataset-resources li a { + background-color: #187794; +} + +.label[data-format="csw"], .label[data-format*="csw"] { + background-color: #e6b800; +} + +/*==================================== +CSS APPLIED TO Similar GRSF Records +======================================*/ + +.my-grsf-table{ + word-break: break-all; +} + +.my-grsf-table tr td{ + width: inherit; +} + +.my-grsf-table tr td:first-child{ + width: 82px !important; +} + +/*==================================== +CSS APPLIED in base.html +======================================*/ + +#ckan-page-loading { + display: none; + position: fixed; + top: 50%; + left: 50%; + margin-top: -130px; + margin-left: -130px; + width: 260px; + height: 260px; + z-index: 100000; + background-image: url("/pageloading.gif"); + background-repeat: no-repeat; + background-position: center; +} + +/*==================================== +CSS APPLIED in search_for_location.html +======================================*/ + +div#search-for-location { + +} + +div#search-for-location #dataset-map { + position: relative !important; + top: +0px !important; +} + +div#search-for-location #dataset-map-container { + height: 300px; +} + + +div#search-for-location .module-heading { + display: none; +} + +div#search-for-extent{ + padding-top: 10px; +} + +/*==================================== +CSS APPLIED in additional_info.html +======================================*/ +.qr-code-table { + width: 100%; +} + +.qr-code-table td { + width: 85%; + border: 1px solid #e3e3e3; +} + +.qr-code-table td:first-child { + padding-left: 10px; + border-right-style: none; + +} + +.qr-code-table td:last-child { + width: 105px; + text-align: center; + border-left-style: none; + +} + +/* MAX-WITH APPIED TO QR_CODE */ +.qr-code-table img { + max-width: 100px; + height: auto; +} + + +/*==================================== +CSS APPLIED FROM JSON TO HTML TABLE +======================================*/ + +.json-to-html-table-column{ + word-break: break-all; +} + +.json-to-html-table-column tr td{ + width: inherit; +} + +.json-to-html-table-column tr td:first-child{ + font-weight: bold; + color: #5a5a5a; +} + +/*==================================== +CSS APPLIED into custom_form_fields +======================================*/ +.disabled-div{ + pointer-events: none; + opacity: 0.5; +} + +.disabled-div input[type="text"]{ + background: #f1f1f1; +} + +/*==================================== +CSS APPLIED into extra_table.html +======================================*/ + +.read-more-state { + display: none; +} + +.read-more-target { + opacity: 0; + max-height: 0; + font-size: 0; + transition: .25s ease; +} + +.read-more-state:checked ~ .read-more-wrap .read-more-target { + opacity: 1; + font-size: inherit; + max-height: 999em; + content: ""; +} + +.read-more-state ~ .read-more-trigger:before { + content: 'Show more'; +} + +.read-more-state:checked ~ .read-more-trigger:before { + content: 'Show less'; +} + +.read-more-state:checked ~ .read-more-wrap::after { + content: ""; +} + +.read-more-trigger { + cursor: pointer; + display: inline-block; + padding: 0 .5em; + color: #187794; + font-size: .9em; + line-height: 2; + border: 1px solid #ddd; + border-radius: .25em; + font-weight: normal; +} + +.read-more-trigger::after { + content: ""; +} + +.read-more-wrap { + margin-bottom: 2px; +} + +.read-more-wrap::after{ + content: " ..."; + +} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/helpers.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/helpers.py new file mode 100644 index 0000000..f436c10 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/helpers.py @@ -0,0 +1,719 @@ +from multiprocessing import context +import ckan.authz as authz +import ckan.model as model +from webhelpers2.html import literal +from webhelpers2.text import truncate +import ckan.lib.helpers as h +import ckan.logic as logic +from ckan.common import config +from ckanext.d4science_theme.d4sdiscovery.d4s_namespaces_controller import D4S_Namespaces_Controller +from ckanext.d4science_theme.d4sdiscovery.d4s_namespaces_extras_util import D4S_Namespaces_Extra_Util +from ckanext.d4science_theme.qrcodelink.generate_qrcode import D4S_QrCode +import urllib.request, urllib.error, urllib.parse + +from ckan.common import ( + _, g, c, request, session +) + +import random +from operator import itemgetter +from logging import getLogger +import base64 +import sys, os, re +import configparser +import collections +import ckan.plugins.toolkit as tk +import ckan.logic as logic + + +log = getLogger(__name__) + +systemtype_field = 'systemtypefield' +systemtype_field_default_value = 'system:type' +ic_proxy_url_field = 'ic_proxy_url' +ic_proxy_url_field_default_value = "https://registry.d4science.org/icproxy/gcube/service" +application_token_field = 'application_token' +namespaces_generic_resource_id_default_value = "23d827cd-ba8e-4d8c-9ab4-6303bdb7d1db" +namespaces_gr_id_fieldname = "namespaces_generic_resource_id" +namespaceseparator_field = 'namespace_separator' +namespaceseparator_field_default_value = ':' +systemtype_rgb_colors = ['#c0392b ', '#585858', '#04407C', '#9b59b6', '#2ecc71', '#16a085', '#7f8c8d ', '#2ecc71', + '#FA8072', '#00FFFF', '#C76611', '#f39c12', '#800000'] +systemtype_field_colors = 'systemtype_field_colors' + +systemtype_cms_fields_placeholders = {'prefix': 'system:cm_', 'item_status': 'system:cm_item_status'} + +NOCATEOGORY = 'nocategory' +TRANSLATE_OF_ = 'translate_of_' + +ctg_namespace_ctrl = None + + +# ADDED BY FRANCESCO.MANGIACRAPA, related to Task #5196 +def get_user_role_for_group_or_org(group_id, user_name): + ''' Returns the user's role for the group. (Ignores privileges that cascade + in a group hierarchy.)''' + return authz.users_role_for_group_or_org(group_id, user_name) + + +# ADDED BY FRANCESCO.MANGIACRAPA, related to breadcrumb for Group +def get_parents_for_group(group_name_or_id): + ''' Returns the user's role for the group. (Ignores privileges that cascade + in a group hierarchy.)''' + group = model.Group.get(group_name_or_id) + if group: + return model.Group.get_parent_group_hierarchy(group) + else: + return None + + +# ADDED BY FRANCESCO.MANGIACRAPA +def get_header_param(parameter_name, default=None): + ''' This function allows templates to access header string parameters + from the request. ''' + return request.headers.get(parameter_name, default) + + +# ADDED BY FRANCESCO.MANGIACRAPA +def get_request_param(parameter_name, default=None): + ''' This function allows templates to access query string parameters + from the request. ''' + return request.args.get(parameter_name, default) + + +# ADDED BY FRANCESCO.MANGIACRAPA +def get_cookie_value(cookie_name, default=None): + ''' This function allows templates to access cookie by cookie_name parameter + from the request. ''' + + value = request.cookies.get(cookie_name) + + if value is None: + print(('cookie: ' + cookie_name + ', has value None')) + else: + print(('cookie: ' + cookie_name + ', has value ' + value)) + + return value + + +# Updated BY FRANCESCO.MANGIACRAPA, added allow_html +def markdown_extract_html(text, extract_length=190, allow_html=False): + ''' Returns the plain text representation of markdown encoded text. That + is the texted without any html tags. If extract_length is 0 then it + will not be truncated.''' + if not text: + return '' + if allow_html: + plain = h.markdown(text.strip()) + else: + plain = h.RE_MD_HTML_TAGS.sub('', h.markdown(text)) + + if not extract_length or len(plain) < extract_length: + return literal(plain) + return literal(str(truncate(plain, length=extract_length, indicator='...', whole_word=True))) + + +def get_systemtype_field_dict_from_session(): + '''Return the value of 'ckan.d4science_theme.metadatatypefield' + read from production.ini''' + + systemtype_fieldname = session.get(systemtype_field) + + if systemtype_fieldname is None: + log.info(systemtype_field + " not found in session, loading from config") + else: + log.debug(systemtype_field + " found in session having value: %s" % systemtype_fieldname) + return systemtype_fieldname + + systemtype_fieldname = config.get('ckan.d4science_theme.' + systemtype_field) + + if systemtype_fieldname is None: + log.info( + systemtype_field + " field does not exist in production.ini, returning default value %s" % systemtype_field_default_value) + systemtype_fieldname = systemtype_field_default_value + + separator = get_namespace_separator_from_session() + log.debug("Replacing %s" % separator + " with empty string for key %s" % systemtype_field) + systemtype_fieldname_name = systemtype_fieldname.replace(separator, "") + purgedfieldname = purge_namespace_to_fieldname(systemtype_fieldname) + log.debug("Setting %s" % systemtype_fieldname + " in session for key %s" % systemtype_field) + session[systemtype_field] = {'id': systemtype_fieldname, 'name': systemtype_fieldname_name, + 'title': purgedfieldname} + session.save() + return session[systemtype_field] + + +def get_d4s_namespace_controller(): + '''Instance the D4S_Namespaces_Controller and check that the namespaces are not empty reading it from IS and/or using a Caching system. + The ic-proxy-url is built by reading the configurations from production.ini''' + + d4s_extras_controller = D4S_Namespaces_Controller.getInstance() + global ctg_namespace_ctrl + + if ctg_namespace_ctrl is not None: + log.info("ctg_namespace_ctrl with configurations is NOT None") + the_namespaces = d4s_extras_controller.load_namespaces(ctg_namespace_ctrl['ic_proxy_url'], + ctg_namespace_ctrl['resource_id'], + ctg_namespace_ctrl['application_token']) + log.debug("the_namespaces are %s" % the_namespaces) + + if the_namespaces is None or len(the_namespaces) == 0: + log.info("D4S_Namespaces_Controller obj with none or empty namespaces, going to read them") + else: + log.info("d4s_namespaces_controller found and the namespaces property is not empty: %s" % d4s_extras_controller) + return d4s_extras_controller + else: + log.info("ctg_namespace_ctrl with configurations is None, instancing it") + + ic_proxy_url_value = config.get('ckan.d4science_theme.' + ic_proxy_url_field) + + if ic_proxy_url_value is None: + log.info( + "ckan.d4science_theme." + ic_proxy_url_field + " field does not exist in production.ini, returning default value %s" % ic_proxy_url_field_default_value) + ic_proxy_url_value = ic_proxy_url_field_default_value + + application_token_fieldname = config.get('ckan.d4science_theme.' + application_token_field) + + if application_token_fieldname is None: + log.error("ckan.d4science_theme." + application_token_field + " field does not exist in production.ini!!!") + application_token_fieldname = None + + namespaces_gr_id_fieldname_value = config.get('ckan.d4science_theme.' + namespaces_gr_id_fieldname) + + if namespaces_gr_id_fieldname_value is None: + log.error("ckan.d4science_theme." + application_token_field + " field does not exist in production.ini!!!") + namespaces_gr_id_fieldname_value = namespaces_generic_resource_id_default_value + + # filling the ctg_namespace_ctrl with IS configurations to perform the query for loading the namespaces from IS + ctg_namespace_ctrl = {'ic_proxy_url': ic_proxy_url_value, + 'application_token': application_token_fieldname, + 'resource_id': namespaces_gr_id_fieldname_value} + + d4s_extras_controller.load_namespaces(ctg_namespace_ctrl['ic_proxy_url'], ctg_namespace_ctrl['resource_id'], + ctg_namespace_ctrl['application_token']) + + return d4s_extras_controller + + +def get_extras_indexed_for_namespaces(extras): + namespace_dict = get_namespaces_dict() + # log.info("my_namespace_dict %s" % namespace_dict) + my_extra = get_extras(extras) + # log.info("my_extra is %s" % my_extra) + # d4s_extras_controller = D4S_Namespaces_Controller.getInstance() + # extras_indexed_for_categories = d4s_extras_controller.get_extras_indexed_for_namespaces(namespace_dict, my_extra) + + extras_indexed_for_categories = D4S_Namespaces_Extra_Util().get_extras_indexed_for_namespaces(namespace_dict, + my_extra) + return extras_indexed_for_categories + + +def get_namespaces_dict(): + d4s_extras_controller = get_d4s_namespace_controller() + + if d4s_extras_controller is not None: + return d4s_extras_controller.get_dict_ctg_namespaces() + else: + log.info("local_extras_controller is null, returning empty dictionary for namespaces") + return {} + + +def get_extra_for_category(extras_indexed_for_categories, key_category): + if key_category in extras_indexed_for_categories: + catalogue_namespace = extras_indexed_for_categories[key_category] + return catalogue_namespace.extras + + return [] + + +def get_systemtype_value_from_extras(package, extras=None): + '''Returns the value of metadata fied read from key 'metadatatype' + stored into extra fields if it exists, 'No Type' otherwise''' + systemtype_dict = get_systemtype_field_dict_from_session() + + no_type = 'No Type' + + if extras is None: + return no_type + + for extra in extras: + k, v = extra['key'], extra['value'] + log.debug("key is %s" % k) + log.debug("value is %s" % v) + if k == str(systemtype_dict['id']): + return v + + return no_type + + +def get_namespace_separator_from_session(): + '''Returns the character used to separate namespace from fieldname''' + + separator = session.get(namespaceseparator_field) + + if separator is None: + log.info(namespaceseparator_field + " not found in session, loading from config") + else: + log.debug(namespaceseparator_field + " found in session: %s" % separator) + return separator + + namespace_sep = config.get('ckan.d4science_theme.' + namespaceseparator_field) + + if namespace_sep is None: + log.info( + namespaceseparator_field + " field does not exist in production.ini, returning default value %s" % namespaceseparator_field_default_value) + namespace_sep = namespaceseparator_field_default_value + + log.debug("Setting %s" % namespace_sep + " in session for key %s" % namespaceseparator_field) + session[namespaceseparator_field] = namespace_sep + return namespace_sep + + +def get_extras(package_extras, auto_clean=False, subs=None, exclude=None): + ''' Used for outputting package extras + + :param package_extras: the package extras + :type package_extras: dict + :param auto_clean: If true capitalize and replace -_ with spaces + :type auto_clean: bool + :param subs: substitutes to use instead of given keys + :type subs: dict {'key': 'replacement'} + :param exclude: keys to exclude + :type exclude: list of strings + ''' + + # If exclude is not supplied use values defined in the config + if not exclude: + exclude = g.package_hide_extras + output = [] + for extra in package_extras: + if extra.get('state') == 'deleted': + continue + k, v = extra['key'], extra['value'] + if k in exclude: + continue + if subs and k in subs: + k = subs[k] + elif auto_clean: + k = k.replace('_', ' ').replace('-', ' ').title() + if isinstance(v, (list, tuple)): + v = ", ".join(map(str, v)) + output.append((k, v)) + return output + + +def purge_namespace_to_fieldname(fieldname): + separator = get_namespace_separator_from_session() + + if fieldname is None: + return "" + + if separator not in fieldname: + return fieldname + + end = fieldname.index(separator) + 1 + max_l = len(fieldname) + if end < max_l: + return fieldname[end:max_l] + return fieldname + + +def purge_namespace_to_string(facet): + if not c.search_facets or \ + not c.search_facets.get(facet) or \ + not c.search_facets.get(facet).get('items'): + return "" + + facet_name = c.search_facets.get(facet) + print(("facet_name " + str(facet_name))) + end = str(facet_name).index(":") + if end <= len(facet_name): + return facet_name[:end] + return facet_name + + +def count_facet_items_dict(facet, limit=None, exclude_active=False): + if not c.search_facets or \ + not c.search_facets.get(facet) or \ + not c.search_facets.get(facet).get('items'): + return 0 + facets = [] + for facet_item in c.search_facets.get(facet)['items']: + if not len(facet_item['name'].strip()): + continue + if not (facet, facet_item['name']) in list(request.args.items()): + facets.append(dict(active=False, **facet_item)) + elif not exclude_active: + facets.append(dict(active=True, **facet_item)) + + # for count, + # print "facets " + str(facets) + total = len(facets) + log.debug("total facet: %s" % facet + " are %d" % total) + return total + + +def random_color(): + rgbl = [255, 0, 0] + random.shuffle(rgbl) + return tuple(rgbl) + + +def check_url(the_url): + try: + urllib.request.urlopen(the_url) + return True + except urllib.error.HTTPError as e: + # print(e.code) + return False + except urllib.error.URLError as e: + # print(e.args) + return False + except Exception as error: + # print(error) + return False + + +def get_color_for_type(systemtype_field_value): + '''Return a color assigned to a system type''' + + systemtypecolors = session.get(systemtype_field_colors) + # log.info("color: getting color for type: %s" %systemtype_field_value) + + if systemtypecolors is None: + log.info("color: " + systemtype_field_colors + " not found in session, creating new one") + systemtypecolors = {} + session[systemtype_field_colors] = systemtypecolors + else: + log.debug("color: " + systemtype_field_colors + " found in session having value: %s" % systemtypecolors) + + e_color = systemtypecolors.get(systemtype_field_value) + + if e_color is None: + usedcolorsLen = len(systemtypecolors) + colorsLen = len(systemtype_rgb_colors) + index = usedcolorsLen if usedcolorsLen < colorsLen else random.randint(0, colorsLen - 1) + e_color = systemtype_rgb_colors[index] + # log.debug("color: adding color %s" %e_color +" index is: "+str(index)) + systemtypecolors[systemtype_field_value] = e_color + session[systemtype_field_colors] = systemtypecolors + + session.save() + # log.debug("color: returning color %s" %e_color +" for type: "+systemtype_field_value) + return e_color + + +def ordered_dictionary(list_to_be_sorted, property='name', ordering="asc"): + # print ("dict %s" %list_to_be_sorted) + + ord = False if ordering == "asc" else True + + if list_to_be_sorted: + return sorted(list_to_be_sorted, key=itemgetter(property), reverse=ord) + + return list_to_be_sorted + + +def qrcode_for_url(url): + if url: + try: + qr_code = D4S_QrCode(url) + image_path = qr_code.get_qrcode_path() + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()) + return "" + except Exception as error: + log.error("Error on getting qrcode for url: " + url + "error: %s" % error) + + return "" + + +def get_list_of_organizations(limit=10, sort='packages'): + to_browse_organizations = [] + try: + data = {} + + if sort: + data['sort'] = sort + + data['limit'] = limit + data['all_fields'] = True + ordered_organizations = [] + ordered_organizations = logic.get_action('organization_list')({}, data) + + for organization in ordered_organizations: + try: + to_browse_obj = {} + + if not organization['name']: + continue + + to_browse_obj['name'] = organization['name'] + + if 'package_count' in organization: + to_browse_obj['package_count'] = organization['package_count'] + + if 'display_name' in organization: + to_browse_obj['display_name'] = organization['display_name'] + + image_url = get_url_to_icon_for_ckan_entity(organization['name'], 'organization', False) + + # Using ICON as first option + if image_url: + to_browse_obj['url'] = image_url + # Using object image_url as second one + elif 'image_url' in organization and organization['image_url']: + to_browse_obj['url'] = organization['image_url'] + # Default placeholder + else: + to_browse_obj['url'] = h.url_for_static('/images/organisations/icon/placeholder-organization.png') + + to_browse_organizations.append(to_browse_obj) + except (logic.NotFound, logic.ValidationError, logic.NotAuthorized) as error: + # SILENT + log.warn("Error on putting organization: %s" % error) + + log.info("browse %d" % len(ordered_organizations) + " organisation/s") + except (logic.NotFound, logic.ValidationError, logic.NotAuthorized) as error: + log.error("Error on getting organizations: %s" % error) + return [] + + return to_browse_organizations + + +def get_list_of_groups(limit=10, sort='package_count'): + to_browse_groups = [] + try: + data = {} + if sort: + data['sort'] = sort + + data['limit'] = limit + data['all_fields'] = True + ordered_groups = [] + ordered_groups = logic.get_action('group_list')({}, data) + + for group in ordered_groups: + # print "\n\ngroup %s" %group + try: + to_browse_obj = {} + + if not group['name']: + continue + + to_browse_obj['name'] = group['name'] + + if 'package_count' in group: + to_browse_obj['package_count'] = group['package_count'] + + if 'display_name' in group: + to_browse_obj['display_name'] = group['display_name'] + + if 'image_url' in group and group['image_url']: + to_browse_obj['url'] = group['image_url'] + else: + to_browse_obj['url'] = get_url_to_icon_for_ckan_entity(group['name'], 'group') + + to_browse_groups.append(to_browse_obj) + except (logic.NotFound, logic.ValidationError, logic.NotAuthorized) as error: + # SILENT + log.warn("Error on putting group: %s" % error) + + log.info("browse %d" % len(ordered_groups) + " organisation/s") + except (logic.NotFound, logic.ValidationError, logic.NotAuthorized) as error: + log.error("Error on getting group: %s" % error) + return [] + + return to_browse_groups + + +def get_browse_info_for_organisations_or_groups(type='organization', limit=10, sort_field=None): + sort = None + if sort_field: + sort = sort_field + + if type == 'organization': + if sort: + return get_list_of_organizations(limit, sort) + else: + return get_list_of_organizations(limit) + + elif type == 'group': + if sort: + return get_list_of_groups(limit, sort) + else: + return get_list_of_groups(limit) + + return [] + + +def get_image_display_for_group(item_id): + if item_id: + try: + item_obj = model.Group.get(item_id) + + if item_obj and item_obj.image_url: + return item_obj.image_url + else: + return h.url_for_static('/images/groups/icon/placeholder-group.png') + + except Exception as error: + log.error("Error on getting item obj: %s" % item_id + "error: %s" % error) + + +def get_application_path(): + if getattr(sys, 'frozen', False): + # If the application is run as a bundle, the pyInstaller bootloader + # extends the sys module by a flag frozen=True and sets the app + # path into variable _MEIPASS'. + application_path = sys._MEIPASS + else: + application_path = os.path.dirname(os.path.abspath(__file__)) + + return application_path + + +''' +Get icon url for input entity type +@:param default_placeholder if True returns the URL of default image, otherwise None. +''' + + +def get_url_to_icon_for_ckan_entity(item_name, entity_type=None, default_placeholder=True): + if not entity_type or not item_name: + return None + + dir_images_full_path = get_application_path() + "/public/images" + dir_images_relative_path = "/images" + + if entity_type == 'group': + dir_images_full_path += "/groups" + dir_images_relative_path += "/groups" + placeholder_icon = "placeholder-group.png" + elif entity_type == 'organization': + dir_images_full_path += "/organisations" + dir_images_relative_path += "/organisations" + placeholder_icon = "placeholder-organization.png" + elif entity_type == 'type': + dir_images_full_path += "/types" + dir_images_relative_path += "/types" + placeholder_icon = "placeholder-type.png" + else: + return None + + icon_path = dir_images_full_path + "/icon/" + item_name.lower() + ".png" + if os.path.isfile(icon_path): + return h.url_for_static(dir_images_relative_path + "/icon/" + item_name.lower() + ".png") + elif default_placeholder: + return h.url_for_static(dir_images_relative_path + "/icon/" + placeholder_icon) + + return None + + +def get_user_info(user_id_or_name): + if user_id_or_name: + try: + + item_obj = model.User.get(user_id_or_name) + + if item_obj: + return item_obj + + return None + except Exception as error: + log.error("Error on getting item obj: %s" % user_id_or_name + "error: %s" % error) + + return None + + +''' +Search the value of my_search_string into input file {ckan_po_file} or the default file ckan.po provided as CKAN language +and returns its translate +''' + + +def get_ckan_translate_for(ckan_po_file, my_search_string): + my_translate = session.get(TRANSLATE_OF_ + my_search_string) + + if not my_search_string: + return "" + + if my_translate: + log.info("Translate of '%s' " % my_search_string + " found in session as: %s" % my_translate) + return my_translate + + if not ckan_po_file: + ckan_po_file = "/usr/lib/ckan/default/src/ckan/ckan/i18n/en_GB/LC_MESSAGES/ckan.po" + + numlines = 0 + numfound = 0 + found = 0 + line_text = "" + + try: + infile = open(ckan_po_file, "r") + + for line in infile: + numlines += 1 + if found > 0: + numfound += 1 + line_text += str(line) + found = 0 # reset found + + # found += line.upper().count(my_search_string.upper()) + found += line.count(my_search_string) + + if found > 0: + log.debug("The search string '%s'" % my_search_string + " was found. Read the line: %s" % str(line)) + + infile.close() + + except Exception as e: + print(("Exception during parsing the file %s" % ckan_po_file, e)) + + log.info("Recap: '%s' was found" % my_search_string + " %i times " % numfound + "in %i lines" % numlines) + log.debug("Line text is: %s" % line_text) + + pattern = '"([A-Za-z0-9_ \./\\-]*)"' + m = re.search(pattern, line_text) + + try: + my_translate = m.group() + except Exception as e: + print(("Pattern %s" % my_search_string + " not found ", e)) + + if my_translate: + log.debug("Replacing quotas...") + my_translate = my_translate.replace("\"", "") + + log.info("Found the string '%s'" % my_translate + " that translating '%s'" % my_search_string) + + session[TRANSLATE_OF_ + my_search_string] = my_translate + session.save() + + return my_translate + + +def get_location_to_bboxes(): + config = configparser.ConfigParser() + config.optionxform = str + location_to_bboxes = {} + try: + bboxes_file = get_application_path() + "/public/location_to_bboxes.ini" + log.debug("bboxes_file is: '%s'" % bboxes_file) + config.read(bboxes_file) + for section_name in config.sections(): + log.debug('Location to bboxes Section: ' + section_name) + # print ' Options:', parser.options(section_name) + for name, value in config.items(section_name): + location_to_bboxes[name] = value.replace(",", "%2C") + + ordDictBboxes = collections.OrderedDict(sorted(location_to_bboxes.items())) + log.debug("Ordered 'bboxes_file' dict: '%s'" % ordDictBboxes) + return ordDictBboxes + except Exception as error: + log.error("Error on reading file: %s" % bboxes_file + "error: %s" % error) + +def get_content_moderator_system_placeholder(): + return systemtype_cms_fields_placeholders + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py new file mode 100644 index 0000000..74c75d9 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/plugin.py @@ -0,0 +1,454 @@ +# encoding: utf-8 +from logging import getLogger + +import ckan.plugins as plugins +from ckanext.d4science_theme import helpers +import ckan.plugins.toolkit as toolkit +import ckan.lib.dictization.model_save as model_save +import ckan.model as model +import ckan.lib.helpers as h +import sqlalchemy as sa +from ckanext.d4science_theme.controllers.organization import OrganizationVREController +from ckanext.d4science_theme.controllers.home import d4SHomeController +from ckanext.d4science_theme.controllers.systemtype import d4STypeController +from ckanext.d4science_theme.controllers.organization import OrganizationVREController +#from ckan.controllers.home import HomeController +from ckan.config.middleware.common_middleware import TrackingMiddleware +#from ckan.plugins import IRoutes +from flask import Blueprint, render_template + +from ckan.common import ( + g +) +from flask import Flask, g +from ckan.lib.app_globals import app_globals +import ckan.plugins.toolkit as toolkit + + +# Created by Francesco Mangiacrapa +# francesco.mangiacrapa@isti.cnr.it +# ISTI-CNR Pisa (ITALY) + +log = getLogger(__name__) + +d4s_ctg_namespaces_controller = None + +def remove_check_replicated_custom_key(schema): + if schema is not None: + schema.pop('__before', None) + + return schema + +#CREATED BY FRANCESCO MANGIACRAPA FOR OVERRIDING THE package_extras_save FROM dictization.model_save.py +def _package_extras_save(extra_dicts, obj, context): + ''' It can save repeated extras as key-value ''' + allow_partial_update = context.get("allow_partial_update", False) + if extra_dicts is None and allow_partial_update: + return + + model = context["model"] + session = context["session"] + + #ADDED BY FRANCESCO MANGIACRAPA + log.debug("extra_dicts: "+ str(extra_dicts)) + #print "extra_dicts: "+str(extra_dicts) + + extras_list = obj.extras_list + #extras = dict((extra.key, extra) for extra in extras_list) + old_extras = {} + extras = {} + for extra in extras_list or []: + old_extras.setdefault(extra.key, []).append(extra.value) + extras.setdefault(extra.key, []).append(extra) + + #ADDED BY FRANCESCO MANGIACRAPA + #print "old_extras: "+str(old_extras) + + new_extras = {} + for extra_dict in extra_dicts or []: + #print 'extra_dict key: '+extra_dict["key"] + ', value: '+extra_dict["value"] + #new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) + if extra_dict.get("deleted"): + log.debug("extra_dict deleted: "+str(extra_dict["key"])) + #print 'extra_dict deleted: '+extra_dict["key"] + continue + + #if extra_dict['value'] is not None and not extra_dict["value"] == "": + if extra_dict['value'] is not None: + new_extras.setdefault(extra_dict["key"], []).append(extra_dict["value"]) + + #ADDED BY FRANCESCO MANGIACRAPA + log.debug("new_extras: "+str(new_extras)) + #print "new_extras: "+str(new_extras) + + #new + for key in set(new_extras.keys()) - set(old_extras.keys()): + state = 'active' + log.debug("adding key: "+str(key)) + #print "adding key: "+str(key) + extra_lst = new_extras[key] + for extra in extra_lst: + extra = model.PackageExtra(state=state, key=key, value=extra) + session.add(extra) + extras_list.append(extra) + + #deleted + for key in set(old_extras.keys()) - set(new_extras.keys()): + log.debug("deleting key: "+str(key)) + #print "deleting key: "+str(key) + extra_lst = extras[key] + for extra in extra_lst: + state = 'deleted' + extra.state = state + extras_list.remove(extra) + + #changed + for key in set(new_extras.keys()) & set(old_extras.keys()): + #for each value of new list + for value in new_extras[key]: + old_occur = old_extras[key].count(value) + new_occur = new_extras[key].count(value) + log.debug("value: "+str(value) + ", new_occur: "+str(new_occur)+ ", old_occur: "+str(old_occur)) + #print "value: "+str(value) + ", new_occur: "+str(new_occur) + ", old_occur: "+str(old_occur) + # it is an old value deleted or not + if value in old_extras[key]: + if old_occur == new_occur: + #print "extra - occurrences of: "+str(value) +", are equal into both list" + log.debug("extra - occurrences of: "+str(value) +", are equal into both list") + #there is a little bug, this code return always the first element, so I'm fixing with #FIX-STATUS + extra_values = get_package_for_value(extras[key], value) + #extras_list.append(extra) + for extra in extra_values: + state = 'active' + extra.state = state + session.add(extra) + #print "extra updated: "+str(extra) + log.debug("extra updated: "+str(extra)) + + elif new_occur > old_occur: + #print "extra - a new occurrence of: "+str(value) +", is present into new list, adding it to old list" + log.debug("extra - a new occurrence of: "+str(value) +", is present into new list, adding it to old list") + state = 'active' + extra = model.PackageExtra(state=state, key=key, value=value) + extra.state = state + session.add(extra) + extras_list.append(extra) + old_extras[key].append(value) + log.debug("old extra values updated: "+str(old_extras[key])) + #print "old extra values updated: "+str(old_extras[key]) + + else: + #remove all occurrences deleted - this code could be optimized, it is run several times but could be performed one shot + countDelete = old_occur-new_occur + log.debug("extra - occurrence of: "+str(value) +", is not present into new list, removing "+str(countDelete) + " occurrence/s from old list") + #print "extra - occurrence of: "+str(value) +", is not present into new list, removing "+str(countDelete)+" occurrence/s from old list" + extra_values = get_package_for_value(extras[key], value) + for idx, extra in enumerate(extra_values): + if idx < countDelete: + #print "extra - occurrence of: "+str(value) +", is not present into new list, removing it from old list" + log.debug("pkg extra deleting: "+str(extra.value)) + #print "pkg extra deleting: "+str(extra.value) + state = 'deleted' + extra.state = state + + else: + #print "pkg extra reactivating: "+str(extra.value) + log.debug("pkg extra reactivating: "+str(extra.value)) + state = 'active' + extra.state = state + session.add(extra) + + else: + #print "extra new value: "+str(value) + log.debug("extra new value: "+str(value)) + state = 'active' + extra = model.PackageExtra(state=state, key=key, value=value) + extra.state = state + session.add(extra) + extras_list.append(extra) + + + #for each value of old list + for value in old_extras[key]: + #if value is not present in new list + if value not in new_extras[key]: + extra_values = get_package_for_value(extras[key], value) + for extra in extra_values: + #print "not present extra deleting: "+str(extra) + log.debug("not present extra deleting: "+str(extra)) + state = 'deleted' + extra.state = state + + +#ADDED BY FRANCESCO MANGIACRAPA +def get_package_for_value(list_package, value): + ''' Returns a list of packages containing the value passed in input + ''' + lst = [] + for x in list_package: + if x.value == value: + lst.append(x) + else: + return lst + + return lst + +#OVERRIDING BASE SQL ALCHEMY ENGINE INSTANCE +def _init_TrackingMiddleware(self, app, config): + self.app = app + log.debug('TrackingMiddleware d4Science instance') + sqlalchemy_url = config.get('sqlalchemy.url') + log.debug('sqlalchemy_url read: '+str(sqlalchemy_url)) + + sqlalchemy_pool = config.get('sqlalchemy.pool_size') + if sqlalchemy_pool is None: + sqlalchemy_pool = 5 + + log.debug('sqlalchemy_pool read: '+str(sqlalchemy_pool)) + sqlalchemy_overflow = config.get('sqlalchemy.max_overflow') + + if sqlalchemy_overflow is None: + sqlalchemy_overflow = 10; + + log.debug('sqlalchemy_overflow read: '+str(sqlalchemy_overflow)) + + try: + self.engine = sa.create_engine(sqlalchemy_url, pool_size=int(sqlalchemy_pool), max_overflow=int(sqlalchemy_overflow)) + except TypeError as e: + log.error('pool size does not work: ' +str(e.args)) + self.engine = sa.create_engine(sqlalchemy_url) + + + +class D4Science_ThemePlugin(plugins.SingletonPlugin, toolkit.DefaultDatasetForm): + plugins.implements(plugins.IConfigurer) + plugins.implements(plugins.IDatasetForm) + plugins.implements(plugins.ITemplateHelpers) + plugins.implements(plugins.IFacets) + #plugins.implements(IRoutes, inherit=True) + + #ckan 2.10 + plugins.implements(plugins.IBlueprint) + + # IConfigurer + def update_config(self, config_): + # Add this plugin's templates dir to CKAN's extra_template_paths, so + # that CKAN will use this plugin's custom templates. + toolkit.add_template_directory(config_, 'templates') + + # Add this plugin's public dir to CKAN's extra_public_paths, so + # that CKAN will use this plugin's custom static files. + toolkit.add_public_directory(config_, 'public') + + # Register this plugin's fanstatic directory with CKAN. + # Here, 'fanstatic' is the path to the fanstatic directory + # (relative to this plugin.py file), and 'example_theme' is the name + # that we'll use to refer to this fanstatic directory from CKAN + # templates. + toolkit.add_resource('assets', 'd4science_theme') + # toolkit.add_resource('assets', 'd4science_scripts') + + #IDatasetForm + def create_package_schema(self): + # let's grab the default schema in our plugin + schema = super(D4Science_ThemePlugin, self).create_package_schema() + schema = remove_check_replicated_custom_key(schema) + #d.package_dict_save = _package_dict_save + return schema + + #IDatasetForm + def update_package_schema(self): + schema = super(D4Science_ThemePlugin, self).update_package_schema() + schema = remove_check_replicated_custom_key(schema) + return schema + + #IDatasetForm + def show_package_schema(self): + schema = super(D4Science_ThemePlugin, self).show_package_schema() + schema = remove_check_replicated_custom_key(schema) + return schema + + #IDatasetForm + def is_fallback(self): + # Return True to register this plugin as the default handler for package types not handled by any other IDatasetForm plugin + return False + + #IDatasetForm + def package_types(self): + # This plugin doesn't handle any special package types, it just + # registers itself as the default (above). + return [] + + + #ITemplateHelpers + def get_helpers(self): + log.info("get_helpers called...") + '''Register functions as a template + helper function. + ''' + # Template helper function names should begin with the name of the + # extension they belong to, to avoid clashing with functions from + # other extensions. + return { + 'd4science_theme_get_user_role_for_group_or_org': helpers.get_user_role_for_group_or_org, + 'd4science_theme_get_parents_for_group': helpers.get_parents_for_group, + 'get_header_param': helpers.get_header_param, + 'get_request_param': helpers.get_request_param, + 'get_cookie_value': helpers.get_cookie_value, + 'd4science_theme_markdown_extract_html' : helpers.markdown_extract_html, + 'd4science_theme_get_systemtype_value_from_extras' : helpers.get_systemtype_value_from_extras, + 'd4science_theme_get_systemtype_field_dict_from_session' : helpers.get_systemtype_field_dict_from_session, + 'd4science_theme_get_namespace_separator_from_session' : helpers.get_namespace_separator_from_session, + 'd4science_theme_get_extras' : helpers.get_extras, + 'd4science_theme_count_facet_items_dict' : helpers.count_facet_items_dict, + 'd4science_theme_purge_namespace_to_facet': helpers.purge_namespace_to_fieldname, + 'd4science_get_color_for_type': helpers.get_color_for_type, + 'd4science_get_d4s_namespace_controller': helpers.get_d4s_namespace_controller, + 'd4science_get_extras_indexed_for_namespaces': helpers.get_extras_indexed_for_namespaces, + 'd4science_get_namespaces_dict': helpers.get_namespaces_dict, + 'd4science_get_extra_for_category' : helpers.get_extra_for_category, + 'd4science_get_ordered_dictionary': helpers.ordered_dictionary, + 'd4science_get_qrcode_for_url': helpers.qrcode_for_url, + 'd4science_get_list_of_organizations': helpers.get_list_of_organizations, + 'd4science_get_image_display_for_group': helpers.get_image_display_for_group, + 'd4science_get_list_of_groups': helpers.get_list_of_groups, + 'd4science_get_browse_info_for_organisations_or_groups': helpers.get_browse_info_for_organisations_or_groups, + 'd4science_get_user_info': helpers.get_user_info, + 'd4science_get_url_to_icon_for_ckan_entity' : helpers.get_url_to_icon_for_ckan_entity, + 'd4science_get_ckan_translate_for' : helpers.get_ckan_translate_for, + 'd4science_get_location_to_bboxes' : helpers.get_location_to_bboxes, + 'd4science_get_content_moderator_system_placeholder': helpers.get_content_moderator_system_placeholder, + } + + #Overriding package_extras_save method + model_save.package_extras_save = _package_extras_save + + #Overriding index home controller - rimosso in ckan 2.10 + #d4sHC = d4SHomeController() + # HomeController.index = d4sHC.index + + #OVERRIDING BASE SQL ALCHEMY ENGINE INSTANCE + TrackingMiddleware.__init__ = _init_TrackingMiddleware + + global d4s_ctg_namespaces_controller + + #if d4s_ctg_namespaces_controller is None: + # log.info("d4s_ctg_namespaces_controller instancing...") + # d4s_ctg_namespaces_controller = helpers.get_d4s_namespace_controller() + # log.info("d4s_ctg_namespaces_controller instancied %s" % d4s_ctg_namespaces_controller) + + + #IFacets + def dataset_facets(self, facets_dict, package_type): + facets_dict = self._update_facets(facets_dict) + return facets_dict + + def group_facets(self, facets_dict, group_type, package_type): + facets_dict = self._update_facets(facets_dict) + return facets_dict + + def organization_facets(self, facets_dict, organization_type, package_type): + facets_dict = self._update_facets(facets_dict) + return facets_dict + + def _update_facets(self, facets_dict): + '''Add 'metadatatype' to facets if not already present.''' + + log.debug("facets_dict: ") + log.debug(', '.join(facets_dict)) + + metadatatype = helpers.get_systemtype_field_dict_from_session() + + '''Adding system:type''' + facet_title = helpers.purge_namespace_to_fieldname(str(metadatatype['id'])) + facet_title = plugins.toolkit._(facet_title.capitalize() + 's') + facets_dict = self._add_or_update_facet(metadatatype['name'],facet_title, facets_dict) + + log.info("site_url is: "+g.site_url) + + #ADD IT IN THE CUSTOMIZATION? + if g.site_url: + + dev_sites = ['https://ckan-d-d4s.d4science.org'] + grsf_sites = ['https://ckan-grsf-admin2.d4science.org', 'https://ckan-grsf.pre.d4science.org'] + sbd_sites = ['https://ckan.sobigdata.d4science.net', 'https://ckan-sobigdata.d4science.org', + 'https://ckan-sobigdata2.d4science.org'] + + if g.site_url in dev_sites: + '''Adding Status of the GRSF record''' + facets_dict = self._add_or_update_facet("StatusoftheRecord", "Status of the Record", facets_dict, + display_after_facet='groups') + elif g.site_url in grsf_sites: + '''Adding Status of the GRSF record''' + # facets_dict = self._add_or_update_facet("StatusoftheGRSFrecord", "Status of the GRSF record", facets_dict, display_after_facet='groups') + # Fixing #23348 + facets_dict = self._add_or_update_facet("StatusoftheRecord", "Status of the Record", facets_dict, + display_after_facet='groups') + elif g.site_url in sbd_sites: + '''Adding the field Availability ''' + facets_dict = self._add_or_update_facet("Availability", "Availability", facets_dict, + display_after_facet='groups') + + return facets_dict + + def init_template_globals(app): + from ckan.lib.app_globals import app_globals + app.jinja_env.globals.update(g=app_globals) + + #changed to migrate to ckan 2.10: + def get_blueprint(self): + d4sHC = d4SHomeController() + d4sTC = d4STypeController() + d4sOC = OrganizationVREController() + blueprint = Blueprint('d4s', self.__module__) + rules = [ + ('/', 'index', d4sHC.index), + ('/type', 'type', d4sTC.index), + ('/organization_vre', 'organization_vre', d4sOC.index), + ] + for rule in rules: + blueprint.add_url_rule(*rule) + + return blueprint + + # def before_map(self, map): + # """This IRoutes implementation overrides the standard + # ``/user/register`` behaviour with a custom controller. You + # might instead use it to provide a completely new page, for + # example. + # Note that we have also provided a custom register form + # template at ``theme/templates/user/register.html``. + # """ + # # Hook in our custom user controller at the points of creation + # # and edition. + # # + # #map.connect('/type', controller='ckanext.d4science_theme.controllers.type::d4STypeController', action='index') + # map.connect('/type', controller='ckanext.d4science_theme.controllers.systemtype:d4STypeController', action='index') + # ''' Added by Francesco Mangiacrapa, see: #8964 ''' + # organization_vre = OrganizationVREController() + # map.connect('/organization_vre', controller='ckanext.d4science_theme.controllers.organization:OrganizationVREController', action='index') + # map.connect('/organization_vre/{id}', controller='ckanext.d4science_theme.controllers.organization:OrganizationVREController', action='read') + # map.redirect('/types', "/type") + # return map + + + def _add_or_update_facet(self, facet_key, facet_value, facets_dict, display_after_facet='organization'): + + #Updating ordering of facets_dict OrderedDict + if str(facet_key) not in facets_dict: + + new_orderded_facets_dict=facets_dict.__class__() + for key, value in list(facets_dict.items()): + new_orderded_facets_dict[key]=value + # #the field 'metadatatype' will be inserted after following key + if key==display_after_facet: + new_orderded_facets_dict[facet_key]=facet_value + + facets_dict.clear() + facets_dict.update(new_orderded_facets_dict) + log.debug("facets_dict ordered: ") + log.debug(', '.join(facets_dict)) + + return facets_dict + + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/.gitignore b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/D4ScienceDataCataloguelogo.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/D4ScienceDataCataloguelogo.png new file mode 100644 index 0000000000000000000000000000000000000000..27dce14fdc36d8805459688fd09ac73fbe133891 GIT binary patch literal 8358 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 z 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 z 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%0A+yop!#imqZbp4O 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 \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/ckan-logo-footer.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/ckan-logo-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..09aa4ca237288153336da6a7428afa9c8a6eecd4 GIT binary patch literal 1684 zcmV;F25b3=P)d((G|hNGdh3;Oo0Uc ze9I-l-hZ4rb^pneC*LWrHZL!NQ8-qHpXWBut3Q<4zI{_Voi6!={J&}V16*#K1|GL-dsdYQ=D95m^+(<> zK;S|s0F4DO{i_6C8rNERc1uJ3kxYC0#`g;lcsCS)M%&c)cy4X~&EtSM@co84e@lSK z|0=-uLKUOjZ?sKs?F3ea57+Ea_}GZ~1Dog1`Fpml?R%vFvDU5-k?U0T6TnQMS5=Ry z>LXzo{%IWB1tPK@z&W=!2!bc$I9{!)%YivS8pr`J1BZRz|1l61?Q^-@HO81vQFxX# zPzzK6rzxb14&V1*919Ad^KH{xJMrzx;DH&x0Tw0q_it&Odo;UkTPZ+3V6FWruu4^j z-@i;mJ}Dw^1EWGYckbLQ&-3=EYCUiy2!iGW&N>mfdc^XDz?ZGHtA~b$mQ_|(z5(EQ z-lMAep^LcxIPeu*`M!TWa2CKA^DE#H=iF&yOt*9H z!@!EL&oH)cJN~=NRVI*4o_^j{Gb6eEti*@AnlaQ>oO07qNLNiXs(} zZv$(9^*~&lRMlsF-@m{vilQA-=+P>GjT5aHul;cUmEX#?x8GI@kVIRP-y=mJ6I4|c zMNa}(0k1pfuB)o5dUGO%p67KGFLSxv?D5tIL2wFq3P3~_PvoTRyY~mzzL;&@ersJ_ zU8z7)BuQFTJH}Z%Tvc%#?-G&E18=J8k}wR73vw>;A&vLn3Ul@i5 zhA)WYc%6tm00>}J^{*n*O`*&#pzs|d$Y z=a2F9Unr_iXYO2jBGa<%J5vP&Adcg$KtJFC&&P3m%i!SP%w*b&qG(<&m%C;(`ydET zIOo0!`}wKfuwQ^uI=SU$oq+y~r~3|GW){0$MgJsDr+oO|9{+i$HsX|3%v#vGqa z%m&dt*{#c7$z->!pK2gQ1wpVopU*D>eoY~H0$f7j^qd3E0EkGhh;)fa|41Lc@3)G` zei7+X)lI2X>bJm6z+nm{*asRYRGmOodw|2vxjr~wa=TRZ-=l;%nC#sp%fGm+W$WGd zY^+~Y#4@J e6uy|t(tiL)!HVKaMX$900000kBe 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 z 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 literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/d4s_tagcloud.js b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/d4s_tagcloud.js new file mode 100644 index 0000000..aa3c487 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/d4s_tagcloud.js @@ -0,0 +1,58 @@ +/*! + * d4s_tagcloud.js + * D4science Tag Cloud which using Tag Cloud Plugin for JQuery + * + * jquery.tagcloud.js + * created by Francesco Mangiacrapa francesco.mangiacrapa@isti.cnr.it + */ +loadTagCloudJS = function (elementIDtoCloud, rgb_start, rgb_end) { + + //console.log('start: '+rgb_start) + //console.log('end: '+rgb_end) + + if(!rgb_start) + rgb_start = '#C0C0C0'; + + if(!rgb_end) + rgb_end = '#000066'; + + var script = document.createElement('script'); + script.onload = function() { + /*console.log("TagCloud json loaded and ready");*/ + $.fn.tagcloud.defaults = { + size: {start: 13, end: 20, unit: 'px'}, + color: {start: rgb_start, end: rgb_end} + } + $('#'+elementIDtoCloud +' a').tagcloud(); + }; + + script.type = "text/javascript"; + script.src = "jquery.tagcloud.js"; + document.getElementsByTagName('head')[0].appendChild(script); +} + +loadCloud = function (elementIDtoCloud, rgb_start, rgb_end) { + + if(!window.jQuery){ + var script = document.createElement('script'); + script.onload = function() { + /*console.log("JQuery loaded and ready");*/ + loadTagCloudJS(elementIDtoCloud,rgb_start,rgb_end); + }; + + script.type = "text/javascript"; + script.src = "https://code.jquery.com/jquery-1.11.0.min.js"; + document.getElementsByTagName('head')[0].appendChild(script); + + }else { + loadTagCloudJS(elementIDtoCloud,rgb_start,rgb_end); + } + + /*SHUFFLE TAGS*/ + var cloud = document.querySelector('#'+elementIDtoCloud); + if (cloud == null) + return; + for (var i = cloud.children.length; i >= 0; i--) { + cloud.appendChild(cloud.children[Math.random() * i | 0]); + } +} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/d4science.ico b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/d4science.ico new file mode 100644 index 0000000000000000000000000000000000000000..718bf5dee1bd4a8774615e59b96ba8dcf13a0e31 GIT binary patch literal 1150 zcmZ8gT}V@57(O#`V3do%f^O_8A=03`=)xcr0_!5k@+P61x{y$)Y|gWtotfiCDt`Juu};MvKeDM99f_x28G7e@nR-A!9!=vOpwon-sU^QqZdqxi7P$H zg_9hH)1ja1MT(V?C;rXZGo0^*$8*PNGIyHQnpp#_nzk8?&i3A}67HPS;r$Ik`{rDE ze=?kfeBt@|;|7%~AjNS&QxuR(38188KtW0YArb+`<PfnB}Zqy^XGYza2)nL%5Q+EODK>f8eIS(S?F#D}UgL{Sh8-H^- zj=n&T&x+;`@hGp=XapL_f##1ioG};<25Y(aAMwMyDc4rTM{kn7$K~i!uCLYIie_%s z>w#89vtrcgfV0u|$LD+g|L!@CjnBW%cT}$sgVJy3IA3>nXIYEe?$R6RQL9Zq^l}irK-DUb!&1%VzF2?r6kV82y$%)eeN%Zo7XuWi{?fa>8 zT)I8&+Rkc!__f@5bJ1+Q78y_$&5v5HF12|pSBTf5_3I(u&aw1(WXJRBX2$r>Xic85 rRKTy#E`Zfl@%BdI@b66t{w~fF%c|i1B0LlDF-!wxj89g?^DyimJkHqN literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/d4science_logo.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/d4science_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3b87305fdae2cd1be416d69711bf516e83b1bed4 GIT binary patch literal 31965 zcmbSxV|*sfw(pZXv27<4+qP}nwryjQOpJ+bW8!3D+qRtv@4Wl$eeV5me&@sO58YMO z3;$ZRR#kPcic*x9fQP|>0RRB-Qj(&|000R4-?9W0#NT^dvyIo^2ezx2rmKpBxvPhf zvl&3h)WO({NXpL0!c5uB$kfYe)QlGZ0GeB=YPxF5$#R=G*wGvPLqqRr=lGW!0N@qy zbTl%tF>@s{HnXs@=Oel9>>(ktGUX%DV3T8%a}+VNw3768HdFDIS2gjrG2t>L5#T4{ z_2m95z|PFoh{)5<*4~BNlaJ(Ia=HJO|7m6*A^I1Is|_E?e}U4JQzQ~`a5f`iqi3Wu zVPt0{VrQafVqxXr;GiX9W@KVyU}R=sVy0u_;AUawW@IG#_eJtoo3p7ox3Z}Czjghs z@sU`%x;k<*FnD-)(0j1ZJ2+b~FmZ8l{Ud{!neHzIor{;ftC1(2y$k7oB#4^1m^fQG zx>`Bd6a6F6$k@Tnm5<~v(*Np$oui!Ge+#yE`M06|nvB8I$dQ4Go{_=M?w@}Bi`vCi z+3f$q_#dfVRJ|O{7?jOi9Ne5u{*H$^>3^92+U|c>^bg@*G~5c#R(}V@$X3+B#Ldpk z-c?GJkL2$gdQ&S?ZVpyX5jG(aW+qlTW$)r@WN%{jAH7z8_5Pbz_T4b#So#j|V7PI=DKxSUNZoiKws<$r+hg+5c1jXL$arx1wgwR_bZ8&1s#Z|;YIOzcJ-E- z`?fS8Njl0+_h+ybWIdgf6G^i{kccc435t09>Zk`gR8BJWyR9&aq%f2O5i}A})ub8j za4YS2YPE|vj@8v^Mt4HT^+$KbRYvZtB!O34SAhGNy_dT#w=nWMHnZaUvset|FY=MT79mE-MpeMd*>0a0FO^90nBr zP9!PLAd_7`c}VC?C})F|?s!nYoiI3;Fl}0$-E0}Z)=Bs_6F{w^q6y|$!#Cj7G_v*%6!5v}4kbS?mvtDavjjQX zBC=BFCKN5-GW;e4dXIZ*NT=T&YCD6-mbZZ-~fU3S87m9P`B%SjWes z(58wN7)$(HHlqz-ymkv0h%b|_8oH31P9jSyn4K+ayGZ8i&<{i{EkKoMw3JY;ty9|< zuO!k(yymGcVBNuSUtppW_A;IQMjuGo9BX?u)^o!*IR}-kMm?bbdV`1Av6s+5H#=-Q zlxe698j9@M|2P=sUIWO(A%_brYgYx%m_z}0B#;8NQrkd53;brod$)NN*TGsQP3~V- z0qhAmvkPIp5GTUWRm1+2hQeDG0Gw-wYaTFs3{6xtVBm2-o2)2N{5gVlSpRwRYCkT> zOpTi=OXLOOS0QDd3taZFGrxBq8^CoC{I@76OVx6p61<`2AkXq_HK$UnpS1p3X*^wb zOdPniE%&Rap~SF9TVKod03RmuOzQdJBDQ|;J_s&&Z+L7YNG4_wdk_9!GyX+}4bt*8 z0PKSb8dQtjoZB6JKEEIGnfSF3ZYaJ8GRK3cg)N6lK9MH0F#%Gk?Wk6Yva*?h-cj)9 zu8YF8vy2lUt9mBqvS$@8)|e-_FyH#Xs|v z;H$AmD5%OO!n9C)YjeNC%bp--*m*DuVBOrR$jm<%Kd+~tN6sxG z?RW82*h3m}G6~ATNBwPEEO@UfMo7~U#p9kdqQ+PBbi!q9_RUZeA!?Ify6qDqiJ`DY zo&iry5*L6k z@NOFVm4^07-Kvcmi!p~XKGsj%In?6-D);e|VUf;`&p9F=;tB_tKR}?D|iS6|6PPh z5kt3N&gIkPc zSc=;@`&{M&+wJm(z@+>xh6v%Sg?m<;A@qyaB8s~2de$ITo7giHL{iA6 z0?SFtDg7IGw`iyxv2uk~E)9g;dhd>9|6Vh|81Fl8xKUK^T_~2DBAEB^+zkm_VlYA? zwn1Bbx!f}G?=X39Zrvpd2+6_0aivb zgblSC}p>*EH;8qId#( zZpPFb(8uhaNtm3+wg0cB0@0SBY*w}6Ws-Co?I(8tZd&{zio?cZXV9hIPY<$9(SXKg zZd#b?d}_dA3i4exzsyFFP(eOaKHE5^#I9?+Szrp2D8~a0nwY;`vv!wM(xG3ASt%J?8&)Ij!XL11nwTqfQ$*$t{{qW`V4JIeB}Wmn?RliLw{PJ9e|h3h zfBVXJy}w2}&7Hd6i098Xo==z7MvTOPI2hTU^6ic*LNu^_mNFhxxE^2`Zw3hywwHB$ zg!yUGuH3y4t(r5?YM4d~10aRy1iB?f1_t_^wPm}z(p4A_oy#KMu9JQE1w1UF}_1g&& zh+uqq z$D*}(p;3%nRR7sFd`%<5fDBSo>hHWrOQIiwe?RW=eqm|v33xAz>h$;!dtbUFY_zG^ zEyKtcfg4K#6IBwv$8(a7rZ8C3D3UZvC+)C8(c$3B;|}=bjrwg2CD}Qh%lG!y?)xbR z0$?)KB!iiV9paQ9CQh{jk~T(}X$=PT@af(&CsgIpMHj2`)u08Tz{Al-HoiFT=*rDQ zMV1t4(t{X=C%_@Xf3z+wwY<$bCQe#NIh6}4(@~+sNH9P}a^z3FJsU{9di!ul4E+;! zw2bW3y~sE!&I?X)(6D4*&j~$zZ>0GOJ94Sxc3ciRbt}6bd^Bjr$_w3 zOUT|43@EkbX_E&K*uBHKxji5A5(@$F?WX7FXJFYkzQ8%xILhd~pv({k04^A%@3V=F zhD@CP3C5?yAEJmo>IMcdE`%#oQ6_c*(j5FtQ*-yLo!R4r%}z7N8o zwcgbW$bH-!tE(;1sCPJIW<^LAq-~Q1L~2A;43nmz4RV8@>yHB%FMoIUCl55Ot@+v2 zTN${41B338A?=WpPVBX2uXh2z9p0Ut6mRgrY6OHpltDE*>icrSfcR@EzO~n_bN;~+ z5&gvnX`|KG(_-8P!BBiC)bllr+Pfd&-MHOX(c#W`>(IQn2gwHXXfQo>cIQx0q6Gw= z`E8b3>`d?jCf1%{U_qh5MkJ#svuvA{)6TC2+}&(~@T0mR^}FN!9-M?jN(9v~jy050 zWQmcmRw~gC24i0{>x{W(lmrG3HwYAMSOms@fOWG^;ve|)^`6W1kFP4Y zzMiG!pKc6x-Tv*foV7~s`@eCivMSQ-YUzJg(IwkiJFExD`-;Dh^&<~1T#ql*dNB>J zaVtJD?hO+KL3W%XLxf2b2)X?9tiHAc> zUTtr;VaXVBphzC|36!HJl4PGINigbun1lrm%7dl?)QWj_V@4pzYxk#pG11f1+PdL> z<_qa!k1?{!eH%_t`0mJg6YR0E`kTaWgAYDV^deGjH3A zrt^fQ%nENYs<)eS^pCXNFv6qYi9|-IYEfTuAS_r67;rXfKIGYP(fQaAvz()ijZx+| zVCCUiXSBfB$cpe$Sz*}4%#RT?4D7CFVLTH=$;%7SrHrt;zmCHO;J<9@22a1dYu9U% z%+G6~Xbx9XqM?lJ7-DMIAAnt3^V$j%2NYFGThCBz|YhI)k^=R5b zMe@e)wPHTSgdv<8e`p~s?9D7|+GA!Xmj!sVSGH?9TxW0dXKPhrj4V80ae3bCjKkWl ziRPB=Yq?%K(D+}k)Vh6b*=*x(1yF*+xM}D-w_1SccYNH{n=e#pM{IleplNgFE44};YOWy!)@**-e-?u z-#RDj;sDJ;sQ{BXKbbAr{ovehQ_A?PE_SG6|n8)}uh8THP9|jc0ymyJ8;h7efQnH%z$k ziBRF7!6eql_{O1qhd^epvh9bK)Q{~T@b|jQw#EWWsh*dZfRp%yX+6cAMGXfE1-{3_ zI;%89-p-euVMARB{AG5?z4gy(8s58v;rPP^mt*Fo^{plBFr9>jhkm`r6c>&tarD;_ zBL-kfb=mh3kuJpIVEpY?+k4hCSxr;uP-`9y@2iiPy^Be>q43O;H}Q2aKlCOjOo7g> z!ySWOSAnOC#(T9O$P(16fInhNH9Yd^o`Gnee^#Hx5gz;~7_`)61*-Z0-aUWfZ_!{%bs4$r{OHM1{lG_fizY& zc8_O)-tFC={LD^Q8*&s1x$hHG_=VT-V4(4Om=X1O;YRvrPhrIwS6MaLU1r_vDW+ET zA*7*qD9)gqy7)&04ks}Q+3UI8cW(eB0l0{EmF>4Ji)fFnKgj140$kp+%>3`Wt=|)T z59n+I0vPg|`L*dQ1)evrbNq%^H1ku8eBbK=?Hma0cob&RHsk0>(GkH&$c@Lod12L3wyjHVnfB<*Wy z$n1F)eH!7G}^S1x_8Gj2&@M3!y&%?i$&CS;3P12D2gRlkP z8lx}~F6ei%W z)%(3|6R*4`Z;

    zq8CA$Y1W(aWbekl_{|b7c2O|{YZp5*BD3h%w?M@j%zY#6Rt(nK!2ZsV$r=J>nAC+ zKNc0-fSMz78{h=!VRGHPZ|JhEZxMjrhDM>MglY7##4)p;GdXPQn${`{R7rq;gvFNY zwBR%AadlOu_1rC459L%Wn0k#HGWX8t217dA@(`g5W-v-^T2HPjjo6ghuodT(VGyF9 zkk#==KsJQC-NbmDC5YSAxlkFO#xiQCz5EDofP5EZtTJen6%4J8ZYrNn(!A{ zlz6=&OaaIhumA?uT=g{sp}6vU^b9To?#E30zO4b7m@yPNm1b3Hlzl9rUiMwA?%Awv z1iz5gVy95XA3LUv-p?D^ktA-|H?I<`-)dm@ z#noCM0M4-?<%9ZRg0G)BIXOzMok(K!^!}OM`qec$B8G*AZv7x^YBNudmSqnT zA6ga?kW^{o2RFN<-G1O$I>4FCT9@-BJ!6&ePY1NaBC_NJjV<-qL-kBjXhgvJIzy7haKarWN**^boFWC@_$en-S#TNq?TbKu2# zZtsJ0omXAg8`NGZG8 zG#gLYbW9eAo2C@^?zM8IHgdg)(;OkuqKgEbH4jRg~hk+sRCF zPff*HceRd0WCdmSOO))SHDhzf&&=lzq;#a4@5M#RQVPT2dq)QctbaC+zfwi}ksAWO z^qpTH+BB}T6YGiNJLnICGR~tqD78BPmJJ>brawluu)-c=(RLl%{A}MRu6sVOI=8}2 zK!%PSPf8mSW7gl1#U@x;D$c_WWQ!H2fNkfhe8tL}y)w{>t2Ha#o(8%XT$P_?oki13 zBTuG{(mB+8pB{XG(Xyz^~+J@p&TQF+atPxUbKf zg*O6EM{7b^PNaR{)cMsKQ<}7RS(Hn}UYxd2qf5!|pb;SaEVW-e4d~+-mg23VI(Vv7 zT)g5+Z!jj3Wpk6_va7lCr{zfzS0>^)B14^6+T$#HLq}V)id<||w%AowyOTE-n2)R! zEK4=f!sTSA;qRZ{2GvI8y;^;?{=s^N1a~LwVPLV0!qZY{YkGDL!sxOoIo7ZU-})+%AL9m@Y!>5tY=H$5255^^nv{0BU8bIG7Tuu?S6iukbv^-S%^cmWr%Y%2Asl$G zx(5=t`1n}nYCffU1SO7)jY)T+P{gVGI@J^HAtuJL5cl^$KQ2rg)M*IB2~j0ir5{Dn z5HcS7pS4hR^~BvAzK%OkhjTNBOWI}-Ba;Z_Ac8Ml;(B?w=@}WkQ9I4# z%1N$))sRl7aX%DYV#5yZ2O1X}>llXZNez~#CKIM?W^L{yi`Hz__H$t)pYdr!l^QKt zyp5PN4}QI7s<3TPS%@U2g>LDA5#R}?kAPW}xp)0;=N)JVKy}vWtgO=NHJaeg-2Fi} z&O2OJBs!l@?{XB@6KehPV18-BL7RQL)|Y&t-%u_S+2shHQNe#<{gd`@K!K}9{^R`5)H5w zLn&mt$uUY6%-7n6-eBoi`LQ43OD>UuISchB(>;iT`7LCT2`8oDc}I<`hBomG{_Jqz zpVSAQfDl=jp|Qk7y4<7yZ}U`IdU~&&@O*Vj_5#(w=X8S+&t-kDL;kyq#_4KUdm9jh z>nO7*w#25^W-nsRkr&puZ~2Omp}`lm6iDDIfqpMC}r< zDQFIkow72hZBU%#PxsT4elf+b#!J~hx2!VIU?l|Q=`uP6^4^^VXB4%a{u-CH0$;H7 zO-eYS5wprtUiYm%K1cW#)2Z4>+${+$8a3_KH;(mr=G2FvgisCDj-?>Z^{bmjknX=g zRxM?)V1r2?;J|cvyrn3rxOfL@AX>Nq{knyod!eFo#iAl?Xu?`%C+SjHkY@3P)v!%M zadudG8o^9X^7FBzj`GNXW~(66kjOkIiesj*%j-FNuaKTJ_=;k`=@^t;QZkEBw)^D` zL~~rE@DtM5$WNfULh_&J)2JasT#-JyZz;-JE2zy&RG}2I1vIA2nUms=m(@tveN~gB z`rt#;?U4@rDEP}@6(VvjZ#60@Dg~g%%@d6Q8;WE6nLdt~fg6yul3mpFF~1S*dp^!? z*ByE)&N>4U$7^^54emWN^Q^?R%u~}nK^qTd)InErxUsm?;gl|oG!S8%LFtZNoS}as z2f78iD;;RDq*4AhU+a8PWyQZ1ePo$d@%H1z>aB`tmi9Ea7tiDh=%&pIzhvjqCD~Vl z8&KCwbuUlzg5=yzkXy2NbOFyYK83AvL5Z!-NS9?j624t?X|20FSdEHm^{GK|3@+7Z zREGnHN1k`ee6*~y9k{kU6iqDw#o`AVhAorqu*E{XIG;7aib;WSz|C@d8VYbdEX;2? z&_IO3LRt@ahaVQuNU`GFKDc=%g!@3&^{o$2$6m{oBuR+m10u`v(?XiLXl1Avot)fq z!q~y4j8rQ9X5cuGEJs5Y@Z_}YmX*3|)0QbRRpKLTI|yPknXv=}@aMaDRAq9x7 zvEa0Ah4L!3GfXjU*rz$yg8O z4Kc_^Hp~_th1^o};4=f`zME1HktfOW7hO{>RhasempRaA9E4I*p^MEPg$T(Tb4{Cn zk^JI-;?n+H=KOxhCuruLSaLjIf#{q#6ryAB+?lBOCR%NdX3e(Q=M;8Xavq;7c>|iS z)%mB7R-@2FwzY%d)&0+p_synK<-j3m=zh)HnLt~$06T-%7FOg_2bOFvOxNdG_*K=6MkPvDa7WL`;hhsSk~CCYybQ2}GZ|TkK6DZLDW1B? zg+tSG-tMm(aH*E-v@^moov2)XQQrtrw)^L06;nrpmKn-^ex60&y0EgATQJviZ^jv{ zADjkWEEDt;y8H)CQEg8e8jRR9sC5Pe^b2=$@5RRZ`uaLgygnRRfV4J;YdBkQU*6N@ zwXa0L>uIURThtB6-66wQK(VSfnF@`QShVpuH?AstI&W--D(8g9!$WU{Nc!b8;rOrn zt+%O7{c3AfvVIIWFL9wa^8Z^Z4)jh@OGI_Ohq>ak`bt1HEXGqVS2DH$%hTqLM zV7{8ck*9;_wgwO(&2+ym)>@Yi|4yB5+m8=92qAoJ>TdeMMxH8tHkYIfG3$o{hy(5^ zh1N5{lXS!FZ`-kFc*d0J6{2^_G8`l76sHvhXPP_In^xkV_{lHRJ(graW^Z=RB5ph# z=|df9)+>^;G+xmFY|sC62=a~nk}u*EBPr9+FD*v)4Fu^TfW!}uS9?~m(G*CF1Ai2{ z&=TFrV*ss@sro@Aj3_M)z{I5 z4;iMAct#3q@>SC!R#wpQ5qJ#5O1bIS?5ueqQ+!EV_YUFqg27Lm9Wr}#Q9gJ*hfI7? zWZa^}gb#Du?2hT$SME!7ZKKISczi#|zrTJR6>038HJn?;p1zdU@K88K6LWFFEcqmbj`UZHCIkr4H z9$s=P=pg7c47O%oQBv#8G|*W+0Z_@Y&h{GIu0#B(X2T$X3z*SAhuDL_leE{#XMWZ# zHRRjr==A4YGL>j9NgprmwbSs9_Qp{qQ2k&6e6tp0LkS2S@J>GGI>B&p9TIV6>Wi3B~{ zyc-ifnf(*sH?D^7Q34TKt$?C&`YM30NSVXhnp0C)i8|EU1lIrN?g2LL*8v^VFWg>5 znG&szA=U+F7vVLg?q#-f3_x}9BtM}+%!JBOxWO+~CJ}PVUkbsn;uPBT5{Eq6D9Ux? z4H(rBA8x@|_>SJY=GH%gB%lX}q;#j9S-zwfX*WHBD-PyVW@H-xf1qC_C#)}NSb=h;W z*(j<6vMhKX#BZ#dDx(Iqoj(^A0m)5GuD#L5t!i<` znA};+xfFpSoE)ZeXfk5PraM89rB|M>Bk%049dSe8=db4wF+??u^NokZ>-FmKnF(4o9eerJ+_pT2&Kk^4FqW&O4^EZ;|!{8J5S^lVey!bUPXWx2YzCv&9%R% z+gb2o!lpCXO7!u)O$k!}AULz6a#4Mw8ZO99Hk}N#n4t=HrvF*@2djm{E5VYcuc3;X zZ9}8l2%Qb?`z2Wyb6WDHvQ(5{0CZiXVz?38qFv4eNB871h$9VJ%;Esvaqj8muY9!V zZ&!Gz0dr9Z*T1wq3|MAT4^X4{S&N3i%yNK z#uhRo5WDR#dHP#Fql)Vshzqxh-66d}1hlpDbMR8#Shn7ims8_GR#ntG3tD4VvEj^o z%?meDzZ-e8fP6KELYyQ?(OUyt9NjH;C)ouUdiE z+14a{o|d|0y;ed&IYMW>^NNhI3?bR+P=(-YkW#(yBX;3m>D`gJ2-rlp*p{a-V0#+9_#vvBuv5(CO*K6DGfPI}uBYbP zUCl$i9Y-zALmRWR-D-Xa69noECC9qA2Rs%w$KHEEKk$!?+q*6~{P~eDUNggiXJXO$ zn%(8h(&pM3V~5f#lo{+Bj(|bDO-44o`82rsntXaBQEQ9k8X*OX7zr7fY~9$Xj(8hY z>a)E9!5nkVikYM}t#y3ZZ9aNF#wx9(;SJkiG6ge#Mv@F;^4?Gomge3=W6*dCEXb=R zyUkGZg~RJs+gnM)mAW{f!o=p+_z5rGfv~hs+w(zpiw^&2R#oJ!(fJfhR`Z>Vx4CzT zGlJ-+^8iZxt0q7ewWuP=UK9Mpn-pU|$ME~NFAls5Z<4Y>Em2t|GNWO7m^QreL|gh+ zMDHy5FlCxO3q1z$v+EbWhK80F_vyc95CBN3H-bv2y@U55Dr#N)tG-N4$Y-wg13+~p zG79-@63D0PZ-{*FC)9o$+B8%Rf?z!@E$Rvq5lV<1Y6%Qs%}fTg7ZMa(rp&i08nu03 zX8X5_u9lpe+%tq%?Bw$!Ql{N^sL~SRJxUuRu?>} zr!{oEkQrMX_kelGQnc8_=69#+S^E_PMfoPx2JyU@0-1}2N47#o##;0B;QW>hQRfFE z%_23}(=l&SAQ^V16A;eAmh=Z3*ftqXB|9re9Ug@-SBv2)g4-@xLAhciqPzQ`ws`nS zyhpbS?qj*YD_5t~E_j(yDO=qdK<^a8;qZFf`ntOOvJM zyvt}10s0mhe&=q!W`guzqZmh>}7r26pqkva+Qm@I#&ATzT zwvK!DzWDs5-xPJj5F$|w47?W&d7#p5U z)6~l&`mo$e6YUUPe5{v*>_j+_zEl|`zm#KcDHB21hcnz^V;GL`hltV_CII($e{QR@ zDHR1ud`FuBCh!7kaOSe6OPDKBo8x`)J!R?%OG~CAj22K>ea!4ro!NU5-u+;=qgqZ?{|2kTYB|TQ@#dudw+%{pk^3i z^c&WdXpwMRoTcAx6paPurq+iTl~EjDBW9^d+Cg6nFM~SadLohqS|EjMjxNG=#-~y_ zYD&#!(UuXb0uHe>7YMN+rX;)K#0LwM`;$o53#C`8H9sya6kRZ%tP&RZ(`)nF<|Y-* zCQ7qho^qx+7VD5yOpLXLg+|`TNdE_66?XA=@O{^SVY}Pic+u)Yoe=;MwPl_DGPi_E zpJu2+%H2~ma)jro2o;*z4ok>@X*CwCTd}szfL;_?J2yrl$}XjjOIH_W78kvFcAW>Z zKvtMVN~uwSHu<^|-N^s|kEOx3p6q2WHQO;17A|gZ4N|I(0Vh9X@{0CkAoz*`;K@F$ zf&oX2dZrPSsnC}c2o19!lM){jUXcEDVOzbhtw7jX6`bUZ(o9p}fJSiwSu<*V43>H~ z&&^dMrx*H%ZBDD&T?IMa(8jgs? z6UG1mvR32@+@b^7F>)&bmGV)uh6lRbhPclRfzr=yxxow1R4!?Li@FS~(nyL4j02~A|TK!jI)qE6^bC528^8-D~^ooCxy zJub**Z|90q;J+i}RK`tAAKu9*Zl7vqXK;F&J!8)oxl<)IZZdAN@?u2rFUBq3;Hc z5c>O1*734AxqejwML;M1)Kv@;@zSdObU!nHdT_u0+jR`=({{kE&DHspx836yaKI#9 zM;%qYiaoPU2aYjeic33fRrRrP9d{V>w?!j3*Kk3YF*tlk>7S|{EenB3?r`G3O^1K>g8{Y3h7$N(MXOpW*JPKZcyP{N)&_G|%x{iri z=*B`;_z?Kyq)QI+`YpkfRl|NSQZE0S>2|NhgDdXucnJ@|^amjR*y)IJNzQFX&+*WB zvev5qT`+7j*cZ~+cj!9sGnU%L8 z<04KR1mNWto@wqfHz0?LLuVvg;3s~>v=-o}v5ZWaESD=U`o+l6;si!6Oz?`vK3QSE z-9z7p>U}RiWenH`hFWqN4g`yEk9LREl5yVxcJ6MJIdytT)JeQ(rFPu5WX#^)@gaP0 z3p5LraWrz5GTr?QLN>hR!0Vq2bv?7ffz{{^bb&J)v-{}o-j4B6PJ@?TI6Nl$u0l=5 z*3=4TIy@*JD$T6w=-kt~E3fGWydVU!fIk{PT?Fp3dZ@0~76Yd0D>Ka^ih~xa7X?{A zGw<(Et0~1;gBR@r__;K_PE3GH-L}*#>}*QhOXDw{cS)IvohP2!JRLZraJnaHdFwUH zW9dN>$NSLnPK=gmRVQn8wnNnONm>V^5RF+RMv*X4lGWyVi8_kIi< z>A!NsQoqUJ-yRkxb!RLfHGw_rAwzoYBfTUmMe zO5AlwOsVGjrx=aLUVdxa#F`e?BYh1DJN>b4u9sYkR#iY-$;>E(N+cT8S^)j{=&ITT z4=7S;u(b?_?y?n`v|R=qX)@x)HT+^Ee-p#Lv4qH5bPknOIv+Y3F-dXt{eaUEbv0=JWyG)9PlS?n;TN#bcSBp=S)y6a`1|qRDjcx zGfBe(tFWX*3xc~-y|RuO>*!t3bh%4rc4Fxl`+Cr`Ge=&G4g|s2Z$*ZrqPJKqASBV@m&J~zG(d^O%jKHy#lA_408s-IGK`WCsxA5%U;2}Y1_knKQah+5K9#C zNjPm}SQ0J9ct#SLRb%f0(gNf03rhGaSV)yhCAMWI>ZsV9&Aw^8sAokNX&c#%>t2V= zTZdlS)dDNUAoGKAbIHL@w@dF1x#}c!VGPOu7uS@euZ73Wq;?qnX4!g28v2q;P#$mG z=Qk2W-D!8xsz9Suk>-n}7FAj`%s}`fA+V(i^sbU-Y?I4}njYlf57-9cD<(Y_kgl<% z#}IfWk4=4{bDpMz%Q58QOb^&qT`vqlFJyo|Xky)3Uok;v$DPlZ&FKWrh1->H;x=g00DV#=QA>|vzx$rkL#eJCwAQ4 z$yAfs&wlo&WBWO)dI?B~id2!9lnP4Wu4-keMp3D87PPk+_T6UNP96U^^U4pvqEG<) z(t5nEtlZ9<8w!gh*5UVby(E^D3Ra6u`B_FfXbe^t#*Sx$f-6CS-)Dhl%Fyc?c7_@3 z1^ADk@{#0K^qB_>M|Ute(=t+6g>5UT2FQjk+Kh$~-*i$tq>lQMEN(Z5E?xQ3;MNdz zn2lU*H#^V+t$w=%i`~{D;|wiquX?W$GvvtnB`7EeKz_u(#dKe}c_f~eTiwkJBzV%3 z$eY;kkKg|IO5cXxmIEI7sRZ&(9UIqQ6m#`=Aut0>fxs^PKd`+=8y(-!uS3jM#{DH0 z8s4T9g!-V>nlQ22FVA}s<)zqs5^8uhZ#IMscpnA(t#avJ1mL_OH7mglYi&Bn2cq?U z@0eqeH99V#1>;2OHP6l#x$VpOnHuGrvx1&3UsDLrD3rHySW`wnUh{Y~9gdl~1eBLO zW5%GoHUwdvAIOa(BjQ=6iMO^U8GfHDEgdgaL$4@=y^qJ=IBG+)ckJuh!3m6kDS4cA zmH8nH#F5y2LxY)j+&ySdzdx!9OqOWn`_PnB`(?|d$W z>}F^^0!CrvEDu_jLbr}=#dAZea$a6;b}wCTY+Jp!U?_)%Ir9@vY|$EClUho)!FgC? zqQFfzX<{?{%3ZhzZ0RyBCJ+p2DYdF>F_TOkiPgfQa#8fxrBKTf55w_8*J8n%R{5f- z8k@7CnOwxw*HSCL(As+8_!=}C&reU|S^uLE<@#EICp^YqFmli;HJjWOl;$?*s1Q_V z*&&{^Sx<9G!S!j!X&B9fIvB~V)hh_g`89V*aKvNyD~@9Vo`sU`zD)@k+n?ks1}$+(9d?tgZxKy=(k1rYQy@ zV>dwVQ;SC&E{a-TVYIOYThC@YSTKCn@Wo5Lgf4`;J7eFk#C3YH>EETCa1JMW5{jS_0tk_}00OILe4*Cm&b?I$@h= zSTHWiEJRi>espwnema!l4SZ9AnnZZujJC>T+R?w0S^w3fM_s-cL70;RN(B1d<(^S+ z{-&(Ic#{F?UT=Z~goOCYO>kyTHT*0skB$bbs?N=(-}?hs9P0N`bBjG$_o9uTpK(DG zE0zD&Y z*f_%u$c*=V_tfX{DX|s)6A$AG5oimsDZ7(Roh<-+uU(_&wtrfR88=@lqtbyuy6v2H z_R*g#V{!&w1ur7Dt9`LtoRMMOu0AhksR2x{g*t2z0_F{w63Ex)3v6gZs%LX>Y|H?a z5w5ejQ)*2yik#vLV;rx*beDH|x;nf#d-5t;xHxD)9X0oa#knEjaEFJyJa@Mch&}9{ zlH3kFm#aB);y(6_zewiwXwlUUXC*LcrC2XBEj5YwHEuq!(8V(Ei)UOq`a|%#5rpMg`%HSf7E4@#8+Bux|4yj-hv$z_1Lo&Lk6ATRmH~r zCYUJmGFw%Lu7Di38oUz%5`svF|GQUkg4eMQRrCt%RxK@!#hmhaRRg6R{!T_=K={u2C(L_Y5tkNMjO+ng2)_^q!R7Z9e)ZJu0JepDi#kZ4;1GT= zL2z*tw!dLeqU(S=_Nwdj&q~Q40E-4@N~p}!`0JzPMSAh#xO$8jI*X_A2zm!sULixY z0}Lqjku7Y*pVx1dd|FRe=R_eodsiGiOeOU6)YwEADX)*55=zLF1H4S$Zv|wHL_CK{ z>`fBNK!}KN=Pz91Vz=uoznguU0aUt3j$HD zQC)^-@gjz$5GMnOJ;j3Xb#3Z5_3s(GdntsdH7(y|=vAxVX9q^K*xrKP_S~h^qUsy^ z0l{=5Zu4@#@Sgsr)gMFzA2#6`PloPWvyQo1?&ivYzq-Mnl8HFB*UnX`GvQ)Y{Xp0o zawX_WCY`j`g-wU!S*1$B8yZ;F52+!joX4xX4(cT^U6EY=CLHvwUh)KW)_k5@xOKu& z#tr|xa=<0y^XOcd4Bs)2&x|o^_uPaSyudMH1*UvoH`u%eaSjLw{`Ix|)DiJho7JYk zUU*&du|xL?uJ@^4g$~4dN01^)Tx4s@F!7ixZB5Fy_AVs)L?^&pSU)oA!JN zk|xa&a#hk+(WJBQci{5*(#>z-)6Bp#9f*B_3?Gnsb*zSSX^8=H{e)V-Q9Fl6@>M% zIoT1>KUOcC*vp6bgL^`Cy}(zr{$OJ{7g&0!ivkck-x2@ zz0E5lvzQ~zxz{}%hiQ4NZOh;QG+Zm0o1gbsBkKExhqibI2;BCsA&UA=uD~uC&dZ`( z0aodH{%kr)CR1N}>s#-BPhYQ>9UHy>1)rgKmBp0IdR_n0Wcv2L)0<8FtLHu2aKGaG zy*#PLbw>SK;(tfMeLNA2y2O~IR*2kkx)hged_T^HvxJ4aTu7p!RY7dR?`R&qZC%pc z+Snb?sqc@8^v)l_F zsXvp`WVK{2^-Rs}VEPqZHGVJ7;g+{CC3TmWRda`}w@H8y!d)hN&{xXBx`9L{?pnt+ z06pVFE-7R)q>#~I^;>^|t~=yD$MYPz8?a721PZ^$c+ioNVNxm;QHi{)Lwg}*tbd-f z93zUa%u<%LB$K4Os{=#0JTVoAChFWq0K9tpdPuQUCeI#y&U4@J_e4M|t>|r$yTH z_8@$}jbf-4LT$KHEyQ&%xv}e6tv0s7t-l+KLWlV2NHj1Ri3*DzqnT4NZieU+GGSXe z!X$VcU>&YuZ*vS2+W@VFSvtg?fI{Yhd`QlW?niN<<5wE4iDAwuHx7^21%qqYk*#h* zEiViguMbTrbda4!T@Q2yWlL5s+^-9|EOMFb&2#mFde)$CWJfpS&>~Tax|DC1xyly6 zgqR#)#st(KS2xo|LyzWchaQVTOIM&@PhsDwx+&QO(-mMUIgCGF**2bHB5VlStRKe6 z4!9->*N9Yr9;XF*&hH#EQ;s-wFJ)E+_oN^}^WiKS;1&6M1{NqA@yFLOUk6|RG)pwA<&ZV%MkAr9A6VeFo! zcu$QX*rL4+-nRhMCN6UfqL_UiZiIQC0#FXljiC*f<#hIst= z@%2{?&x+Q{lF5k@(PYD8W9#8C0MF@AC^QFvGPtaApgc1;0852*cXyNX=ckAuaHQeg zeQARPxq#i_4pSvyUKq!T*3SO^um^bEU0q(bj^~~~2F$FwDxVw1oZ^;yv3S%2K;JK} zDJZ;d)F5@25IEmxZ)<(JE;=Jy>scJKyr=k2 zi*KSrw3~ig=W{LZ?)fH6^h1>6&QOXg&;8f4j(Dg_V32#Dx#K4X7-361S^U<*JM&-8 z{5(Jz6Mtj-6Y1X`_!P_ut^BPe{Qj*lfCb;HbtBqYSZ;aR6b6H`dP<5@T-}@Zg|EX$ zwu0Nj3$5?%`7XCT{9VJa&Yu0ugh;*y?7BxF+rD=t!z8FQoP5pff1UVG%5iZB%cld3 z*z@P3pJ%%H@8t^Wxl?~UuD1RVBqh}$eV^V+wY~>BhS=Mx59Hrf{MO=aW=2oL(=eB; zm2b?p0wdu}`aL}l2S>vT(;pgp2VC1p2{d=PLaD1cHs2qtw7k7@V)0L=_Ckkquq-hf zS@+RBg^97%)IIG_#oyHWb(&!pYL6CQm-*w#_XDHlUBflHKE3r%*g@jud)?&7WEvJ6I^JM(?)YQ}mPTyhW%@#_y{QUv}Z0*3da54GDw&}$` zncjmw>2MFN>(91*iXIMr_lG~8)^ZOOtZRQ{5)|OAnni}bx{FTS*%c8Fm+o5leX~)gXQ=f+p%R^>-_!U&kEPZzxMFI&uQn+m5l8>wqFF;Y?lPX9NB$ffGkdFb@sZB zf{tYirBdm!>FJq4FEM2OJOramW{K?Hbp?q7xS)Z7-GB=TGOK3r^YwRjJu-kHJD|ch z-+Uu^_SvK4fd?M)9Dwn@>*aI9B?_&inCPxj(S4%>SqWZhl#7M1cvp+v*etRy?Td5)fgwR%(?lTuWWA@jd~{&sa%A54F^|6fR$`XQTQ&TRS8 zp7P{-PuSAo3aaHBvS|ud$q~4&m6#3*T$S8jQ=Oe0?G|m_-KvdCX_&|!TXq%e(wdC= z;6PUBQWOus6GpW$tX#xZFXCw0Xj2Y%ilK@0bTzJw$vy2~*ijZ;t2S|4KxIyG){U?2 zA;Lfen36kO`F{Q##cwU`^Gw^YPyiE&=D)b`OY#3aP_|S@<=)#dBcGo&BhM)H6^%~l zleJrC{?qt-%|#uvD#2u`M~_e!Gt$CO(2lSW#N|`cz~GnnJg!fwTa=?!0f2k;xfl!- zV*K(@sKaNq+|^n+!y#71ipJm2X+m4K;O{QhT|u+IyFL%_1GT(*z&R1|@$x<0JWa)4 z-|<732Rrn0N*l&Qqqwdq&hF}>e5jIBt8xch)5Dg-Y0GuMe4V*0+U*E!T+y!CTa}X= zD5n`O9It{=sb~&mW~GioUaOW$s$HuY_C=B3hQgyshSY#4*2)f9FL(g;Q*Etn-1r%CiX)9ENHRRbmmfVGtpB3OfP8 ziDwu|c83Mdh6ziDabVH`+5?!v-_8zcXXFpIeSD;sYz=&qj?m8!{?qQ9^n<)P^`SAZ zlh70o6C;Ej>UqvnRJdBPwgosc1J4z~@-Qtsl<9`w*ac|JSdQUX7wcFQ(%BHhniFCU5(?HS8L~}sQf+dT<18qKM|)GIsjMU z_@o)0VeNv>;<*Hq7DE3;ZfR4w&VXs@wzX`aFdc{Q4no5*JhUn1q*57iC|ceO7#G~G&JKjVakzEgG5Q^@PXnr_cSt0*v} zj5X#C9{Mp#35=)hK~0!p(;bvaUEz!?`)vR<1QQqsZs6J$4bO9JcWq28`h|M4O?$o~ zIEqC%vQ4BDax(Uoo<;tEjH^87`EfVeW=Z3eLxrHH!h$OSyi}$&V4@>S!z%^GlM~ny zwk?O|yhQ^SM|dVhbNk?#w&;fUOd4YGnr8y;3}asK4OVCq*9_97l+9r-2{jeGwr-3; zEBLCk=5AyR#u%bcsc~scxt>ZVP_z{q<*;2rYh^4u_G&S4#pM*;Xf4Chvw*ydXDeNz zXk}(2TUbg3aP;$N)FYd{6gD;` zz^d2f@NO+ad~prMRu;t9@JB}rMb4b973EUhB){xB+;}`eVENY0Scm=J#zv8e;I-UH z)DnJo$9#6Nx}YjXjcRf*#Q^wZYdDcua<&xScckCSY7|Z|)24gGnlWVR&e^uMkW$x? z?aY5A^9K2t7{dD{ezCs-6N@c>c{=I<+pPV~?6u)fZ#~D;%tH8@L~Z`svNm9N+P1Kx zIZ>ZQAuCO3p^tB$&Mk;}T{jha%~pLZ$8gbfD8_xl-s#W){JY}fqGM&<*QJi*CMu3Y zwU~yd3koFLqsi!beNa7I>;vZOEjE#N57v}D;hFM64YvX`8vdv8wor(BO4^R#j}G9- zFv9O2snmLynQ~StU~aVLeX2rXKG>Fuc2l1m8>xM3Htb~xrkSv&QN6?>p=Flu?VUlT zB+2Rqbcn$>S+Y(h7`Msi3%lI1s5k&yH>J5zPYlETK4O9s|M<>Ag|l-?RjaKvaR)+t zFx3)nHUGomA#1G0Q0Saqwm9}zwmhYjm3f%VyzRJQmY!Ae3xs=a|8C^AM zq&{})K6q|jU4`$qBI|NFBWBC9a?mhrjEtd|-90QvZ`B>O!qTj}v3?uUzOf0&8E0w%h>g*6{_~CISnm4A>)3#GFy- zx%V7@P1}DS>PWn+>mOCi{7K{uZRZ$IwY+*!cD=o9{x^GH9v#_L-g)2tYTvX=rM+9d zw`p%+$6(B2?M3oU#3Lb;@A zW`POoNBy<{qMQ+{H=au+7?B+dk(v?NmWu1Tivab>f&Qq3AdcpOe0$~o!qFgv(j$1{ zT_bhSb42!ER})^#A;x!W5%H1cZP7cDr&G4Gs{ca&k_u8h*AWJtHS;LeR6O3q-bJp{ z08`TJF7*NC&zcRbCEhb?G5iEuH!XQ>u3D77Tg9@gHTa<5xn2_XJ~BSjJ20W}DHwb- zfR@DwK}q+Qekj+9bJg>X>(zixEubl8>R?}g)TKtKu7jwQb+RIx&FX4%5&qujLOt=5 zNiiBb%sOm_8}G!JVM{n5r8MJGd^asPt}i;apM-NIUa5l{_?Q6N8|rCnhvi}8=VsHi zXQf85R%&73MDFg&i1CbG$_%rh^It|QaZjSmsh1_({L4xP=dBEPVVm; z8f`FH`2Gk9+cy$vU2_+ddTn)~eh%oRj_d-?0K4aKe}BaF#;C51a2!A4_quk2p=1#T-uGh4X4k6-{fDHC;FCpnevu0mpGAtenAw_D;%8SW-nCQMSut48_h~ z(gHBJQJFZ{t={Ff_OK-((!$tS{(qf5d*+>>X^n*${xs=eFroH^ig2yoJ7xIwQ}gk@%EbMM|ge=f@{Vwe``?YT7AL~WH?8*loiOCq-qn&NdmZu8Gh z#XQ+Yvx1a9aQ=vSUVTr`?~D(dS@Iy~`U||ku$N@Y`a!FvElRQNB?O!F||Bt~Uz2$-j2(FBX3i%t=iDBayp?%WIyw2&OX|f6K6_ z{@p?j{W;3tEls6vIQ$w|^YhWsXiXc5n(dThtT~xuzP#3-Gi}g%JO!~5E`mBj zLS`r)sBT1*V!f=)^VBLZ@1>i=&T+^`xf`%eJ5jH=b5c=~GM-(WH?cl7z^N3N7Nk#>~BAHv1EjTnR z?URi{;Z4FB@Fhwb3|rh{0In+ zSlQ5}@=CMZlJsTJOcsF=sfmOj#1r(iD0v54T|Ix{3^`!kR8-aYp=mk`9!1nlO5{^% zW*^Tx*I$&OK#{AT6LNvAD9T=#BHX4y25C?L8l<&*K;ZgMcwp9X3jI*v& z?t`nDtmDj3zBkO!zKw2it7U2Fz2f({eDvG1r>a#&2^um}ZMs7Zu?h5S2k1iW!7HZS zczs$NU8E__zL-V(ESPisB4O=bn2vWzU5OwH|k?nZyA!}Sw=PPJJ(~)!#;`(X?&(QWX@{8(*H-(1I(fL zmjfsqiadWL3rr?Yb}0GYGg(HYujf4PuulaZ7>Cnc_0aE6{$2jFilwPNsZ@Wz zFtv_xvmg_WCiVrHvHtYjNSt_Sn)a__+dVPcq)VGL6{(+SrJV+1Nz#Ec-LYNUrd=Q6QPwK5z=f}@W2`rg9oazivOc?dQ}mtr z6N!Ye%ro>n4A^4&_%$@mQEM=tZE(gj^$cud=CY~0pw>oj>&;KTCqD;US9a#yf~;5? zs}XN>BF^raPSW&qTFdt~4{r#AO)Ky!0u9VjnlDf6OK?+@3Bg`zY8xPzFi-9N`4r#F z9|IL|8pemEXq!m*K27npnw69vttHU`@9RJ}j_2Br=diy2V}tBR*~+j~1J-H*%=Zfp zPjsISqFEsLSuQG4bH+aQg4D3~)XMsp)X-T&w+g^;7h%2&zU8;Z2IK7XbdvL`ExB7C zp@a60bPHYu+oon&SewbiNnv6v5iw7j`i}$fA*4ZN3k~8iq%NhwlvB4iY0&X(+VM7f zdjfNG78R!P!ILTAeW@+HQ*ubp2luEG|DoqAm7-FRWlN65IIrFA&qbhsk&(2Q&5Ge+ zGvfKo+qt>(c9_VF0ws1&)ye!aBmT1p3;)P#% z?(TsP?9GdJ49o!m$1ay+iv9)dk#!!99S^@pJ4=E_O7cD9lXQ-b^Bhwg9EfTn$DoSg zbu=WWsL{<&%wkVXtnKT==u@}4>Dy>v(SgI5m>?$iCLAe4)$O|DjN}vPh)o|^`RL+p zzUc*$H}$KNk~5Q{vx_d1h%;(#BC#g^#`x;$`%aG%7(;z#l1pTNG)eW*y^|*Cv^S7||rUbi&qhbc5)Q$zCMnX@R>0lmeAJQ1-r6lPFGzq_jZ=zXBf|A3<-QfQKPd4E9hG#rUYYu!v}lSqM488_T; z!+(C{(MKOeKV=w+q&CWzuxuJ(y!Xyb2W3rIOL}o9_b`v`MG;G&@dtfP5%`iJZFAQS zk$KTXW`cr$@8+9c@xIB4$z=qo43S1iJ8k5uw%s=jv$0mFKWhv`jgj|HugCj@Ths@N zN!&bV-C&CEdhTZTca2eWX;GIerUK#+j%Hj;Eg)fA?l523be;94nJhGo5zu5xiV`#u z-rRVMaHV)(IG7GmU%^hX&dXc?+s=lb6K*_a(INxf8+KweqN{iG6icdB#=zb1L~^9{ z`Smx-Pe}=k0x}8Kb9I-d9#|jickC4ALhC{14_J02bFcEuU9 z#@TTY7-!M6U~J;j-W8QuQLG)HEuf7Be?8P#D=+n z=wTh_mAQIlf?X0CS}X1QeV3*Y1x{-$!1sc*1Ur0Ta{xk!e-#AOafEvg44FS}k=&d^ z=!pmj|0n|Tjpz^6-#VCLQ$s_kk};EhrdrbK3yaO>xkbsEI1V)Z<`=&+Mg}Gl z;Zh!viY%d-t!g?t?o+AcvtZi&_K6cG@5e}F$Iljm?1g?UOxVR*Qv5tR_P1(n+cC|n z)!f2Js~6z^1P%+*lg^Q`u`%-8>C;5lbTU3Z_Obo@_kT7(4q@31w#c(DL}T5B zj5GXRGT-~|(OP0M_FCl=^Fx@Eh8w%~;n_St8Zm?XCCl5st_(iMWOy(2p`(%-^{TpT zG-1hX&?Ifph?J_LDb<>ivP?H>JBawv71{%05U$HPi9bG4lO?Uji?l6%y*kqRtJxk8 z?uQ|C8piH<|DmSS!^wrRw7$GhugEoZWnv~{CM5s5^-nHegQ5JO;7GFR+8>c4T6U9fHx5n>CpNC^XvpkdHWQ(B_)VgzHqTDnc?3_h97;D|r zjYK$Mcp{tU9|sV26NZ&ASdn~)x_HelxkI}6jvLiYN1iii|H!EQ2B1!q(L%9BCIgb zACC?VMH5HwN>L25OV{`LApvI)CznC7-LSq{nvFzwadqv#CP z3m-YU{?UcETT6z(WjQnR*5OCS?-{-q_+W9a(DFFdCog7gHY$e-ebcJfd(wgV(lcu%*>lpg{p?@UOR_!Cs)jlC-|GP^&m4dwqB? zii*3x;o=c!Uy1tCN<~@Fb!(lWgC6u4B=B_2La+v}AD;Kfk)A|vUj)(yQ%?*Zc!PG|+>E`f28ou|vMx|vCeU_O7#I>)JzJhHNei`-yhIUSLG}y> z-7!_WW}3EyKDX`G2MEWn8Ag4vP??2Cs^?$dvy@8jf0^=;^P{e86XSbTvGU#@_Qrp2 zzsvMe3gdeVy?xQb)O0$de01qnb6ICWnv&oZ=Qi^eE`Cs)uWD161d3X?a{uyF&&`?F z2?Ii^@ZPg`YR^cMAWQh%#}9v3$VV2HMnIJQ#q9$Gk&Q(YRNAC~wfG&UH{mSHOSUn< zUXng;NPjwU^yP}WELD`1-k~Hr{Kxz2)E}QeVmw*r-KxpdK6EORteGtR#@=In5ppJ4H7=M3P%1}pGdw!QQ8GlF& z#A8!YVKTzG*A4cFgKSX4&>73a(yLqOI1{8cn5%|htXnfq%b8hU*H=8(HbwPFIe-wDyVE~df7 zRA&FNfCgI~8ho}l{$CHcOdqAtu6scc=;avVyVD_43TPY7Y$VB3*}=Hwxpq_FX{CcR zhfQ>Nflj27+}>=Q?*Tik1g95MZ))&Gl@2tnFaeTe#5_rGJ)r$Y4ItaU_`%vgd~_`R zsyjJaT*Ev&Y_@@L==d{*_^!PTVsJ8X*2aNXqO-W|i>15(_;G4_^7j-~O#t)w4b+6y#rI7!`+~GZbYvs0S8-J-0fWH=2RVHxSxbEM;2X^QE?jSX zx5`mnnc@DE5dS-RZRmMUw~Q;6{uM$3X)TOEF{^E}gx#Sn98Yr-++G7#-XK1j7kcp)Ek!`ER6=f zj~MG`K6JRG_i%=xS{}*-AS48_wA+M5@lig)6|MgI`%mUjN?UUZt$(U`UGfk1#I*0X z4r!+qp3QQ^_;m4QY?j}fv@u%hZvismx2m*ua|5 zZBw?iVyW18T5su1dSB1Up?mjL=zA}`N`Isj#U$9~pPorZPd4}Oeb?BUIu<*gO><3l zRZ)Gf9fSY|qyN)#itXobh6g-UV|;^9TMjp>yAiu=B~VMp01Cc<{X3rF7zq7@t_b{_ zr-J4CcMCmSoVpDt__`8}xbc3s$%jSfQKxFNfln0f?Q(3SSu-cA6&*3!8d`?7w+Y@~ zwqV@Hln5_O9`?Z8)IYVDh5fn(4(^dM6FkR7bD|@5QBQ_ldP1$;1t#Ix8;P@2$Nv=+ z>nb4)YCwY?(BQ<&`{t8>a^UsKAD%nplnkIjDWt*QnlcaHK6uu(?N&C+#~Pg&1BU{rJ?eIWF3!5Fw;M);&yW@AX~K9B!eReI7x-ipG?BhFvVNWi zdzuF|-6I>R2Q=-1x%Y0GqMhPu@i(z-p&Qr{nHw?|yaopP$XcmPoOV6u^A*bnk<@m( zYt-^E01=Hw1Mo@2V#8+5SU*~?*8@$kqeWt1ps&C0p*P%d`!AMCr3NxQjNW!@lbb-4 z!3rBbHY(@<(V&{-xGw9`mXDon+a!o+q^=#dybm;kmhCPsN%f(;cLFwEvHRnV%pc7( zm`_ArseNfR5jZ;Vdk(^q!)!QElo1%e%i?$8Y{O89?=lPYyx?*{-^wsM0~Bj9#u(|0 zkVN+$W_SnAL8^m$=V~A+*TDRHuF=pM$(80DGn1+({_N0Cs~kEZ!@}~!eqBmw0{iK}5O#S*l6>=cwa?jh}!4IQp7|@}UEU^aprrKscj62(uj0??@{3DU4RT^Q zhZ+6EfF&E`iJoC3b6t@J2DkzfVUD`!yHXuW{`5kWgdmQ6JB@|aEtDH3o?8rVScY zLK;jg4F6Q_ouf)5#u{~OcIt&HnOo3l>2KyxYme_lH=B`Cu=ozd3#~E!duPtnrBkQM z^08~vAv6r058E{Z#)hWnf!@2{_nqZGd+L1cD}OtlzU6In(4pbt%tMYa@fJx<+r(bW z6SawcgLZq{fDt$rXh-i7Bf{E+`GpVouFErB>0BM=hRmwBCr9eFdQd#y4NJ6HnGh!Q zA`^?jE0Kr?$hsKa474&iWM=sN5QB|sr-6__gY4df zu)#wSA7uS#BuJ#L0P}5v={SaSbzl?Wy9zLItn_QvYWiv-!B=^nQv9KW68rEzg?xDK zCg*HR#6#TpRAJP3R_ZYqwFnyrLDeVP`t7;%7tWSeODl~9nA3$&y(dn0I$t~iB7vud z^LO>@?31-OcnzDujI+we=X0rdOw7fL24$U5GBg~VD@tL7XJ>MPWtLe{SIE%zGvtkw3S7 zle4VH2!93XAAYEHFnf}geP?gEM4Ri@qESxm%g!d>Ink$nsdSB7v$!i73(rZGcm11l zQgxKAREu>NXk)XAGwit1axHHy@O#o>*9b*54IPa7PO~Ijo7T4*Uhg&OCNnO6JNxeO zM_Uh;U*Q&w$Q92C!#guc)BdUcQrR;~YEuUaj+G*=-(&=46tz*u+kpl-N(CBJFQ!5H zv#ZnAtol~}1Je(HjxGTs&(oc})yS18?1)Mu>t3vSLC(vvy$%*l0Aa+23|WNzPi!z1 zo=G(i^xgH#PZs|2G0=rudwYKJ-LQ(6)YAg4e9x^k!ogM8Gj%Df-3z_M`9iyV%V*Zt-+ zX|bOF;G^@}`SbO~iAh&=ETd_dfI(4s(LS<1X`@-Mi1#%_{Y&P1hYu+@@W zHVj9_Qj=7QcN+}@)#CHm=D|paE#VI)mT8sjh4qhtR-*&MZp7n^f@Qo-)nKGOukQp{GvGZ$j-9X&zIBu_gO z`b{{RqTHaf;|~672cczs*Q!~jTwZAuRMkS5a>dhKXYy|jlEi+DUDz^rB>uI&e?ICd zihr=)Fndd?UT;;j7HB6n_MDpj^br!7PUB}K;ilA=dcSau3ZLCyubbJDrd8#-fgRXw zmZe>cq%AH=tK-D~o@2RMYqpYKsg#C%|A z%HC;x(9!_1a>4%4Dyz+PxoNDrjwd0)M(l0r!Ev!mF6b~$p51K0n zb<9-Iqy-rnvjM3T`k7n|p2Gv@a+$9V4G#XdY&Q37xl{qhh#Dn@b(R4`<#=|BPK)D& zeI6*scHBzYw4NyIdZjL#2V1Jy3(N+grsr9nT8qVn^9{w^*J$azx@=+}f+|6LG#<2z zu?R`RFL&RyhdlhXC3CeXuOXZ-YJs7xUerOVN{u-uT%15MQ& z)+7@0D=I0w1ve}3V6EC_HeE^td9`=T1Xj*1Xs)bgaqF7P6VfdzDFT;D-32Pyq;N<@P z_(r-MdS675CBwtfiVBQHHN16Ia}OJan+N7jytB=UATmu(U_naKXkw*^zmfImZrzuNUJ__lg+sUOpOK~L5?|n>@V-x-hwtZsR--=I2!?pu%?UeUnYs-O(ozi5 zUV(KxTU?P=fUp)kny!E@br9t45D9@5w@s;N_awy%1rNqC4Bc9Tagjj;r9(H|GDs*f zSR18j*v}O#U8~9Fyrh~FO4D?4ZLk3hj?cr|@JXa2*D9B^_2p&x8N>D!IGQjgG6J`X zblrdZIRUeVXK-*%Ao!L*37V}`wUtPMSypwo0QWusG??2;gE64N$(?9$1U`c}j#Xxj z6k{@1;{;~TH2o>l@`mBrqrfLRO~3se7_M&tlQ0yksi~G?+ulZ#>#(HCw?Iu%LD||0 z|C3L>{BLbXnwqOW{JWXRMBfDex?iSgVRah{1p)=}oGTHITOe%h8e*&?qiILG+@RZf zaWtQQM<$c~T%l0-ZCO^{0MeEVy14}{`UrXG;YUbB43G_u8#zMHHuv7 zjt&|&!v~Vd#PRRK$9HVyBl3}8Hp@%0{%ai&aGzCpwi>*L{tj#?f`Enn^O1$3m<^$x+nRWLo|2AI( zrU^oUT8F(j3!egR0d%6-YsyKh0f(&u2dV+%^tAnq2p23NJq2bceQ~xP<6oGoYL9$l zX&p2od`5T{f_^uQKw^JU72(7a36b3T>Iw3#Z!Mb7JY6cXEQM7`EA8eVF|ygvImm6q z1~P5)bJcNt<=Sieof~f)CtIPcu^X7=e&O@8CDU{b%q7C{{Gfi)*TT}4VBgU{l4v(b zp)Q;1><4`j5Q@441lk-KiMt~M$#y`=x4-f5VzbdOFTgxC_|8fR<;6+69 z3Sr!_;J<>s9r}A??Vy)uKm5>QLsqO=&&8%c{z7}c^{s0Yf*Y?EWb^p((we{rkm9SI zIknM3b_~F@ts9(^+8(#Oyd*V2a3eIkwml8{m!rXWo46z_hYBmTd|c;ro9RDqFtO{o zuy;ePGheb9Ql|nW0;+3`1Bu~UBq@x%zc;oo(J&U@ey;j|9-qnH`D+O=_2?zEijU-C zuA4Bq9oWGeY+e_#J+(&_X=Ann2G)kwBiAgK{hd{PQY zq{V<4UYr!%9WufsUc>N7XvfTIc3VFRSO*)BQNf#5qCy*kX%1kKHF zG}J9ZFP~448(%))KmJ%j<2bBQq@?cg;859Zh6T?E8;VRbXv*S+o9w~34rw&xAX0tgNFM=7?VLMTm*7wv*x-ba4#U|}rWC+* zBa`8I%E{+r0lfO9s4l1}6Hk`S7L12c+j9HoqYn@1x+~E+&a8zT$TpQK!azuqe0TiL&JHNLwNWrkBYbN ziSxycw*x;r{<9(-CuMCGF{`&Qwj@7Ds_JvZ@eQtJuKjtT_0&5eTv{JaT>mFYA%7p` z)1ZYmNJFWr!fs!MKKLwBb)IENvQTG#an0 zHygK^x_OQ5Sd*q{fvpT|hG9Eh+Pez!JuoRbndi8rWHNqA6eHivW;5S%Z2OELL<5L+ znzcwY=aT%M1c<8!(M^?%Caw#f)AOAj7@J4p;vjkE>Eq;yCm!#HSfaMUIqqDp=1jJ49-T5;PYjyUrqM>c4J*xgNq&W*LPgAo%X}O z!e`rdZ7#YGwZs1SJIwV;QY`!{T(>L9>-DcNXS0R&0V->(>8JdnZq~=I2m&>oMtq-(XtxPJJ zOfxJ)Ns^4JgLLq~{+iU17D4kEsd>0ge_nrBM`pKAB9Muhr@SJ{oF%94`=sHiGnyOganZtwnUT6^w1W z28kyeGBzxbSg%7ozKZTrLb)O`swv4_`dQmE&x4%!s)wm*zPlr1!xw^O?qsg+Uc1-s z_3z`l^0F1mCyr;4>D+7n5;U#Z`Nr4(%5Ym#W#!~AHjSm9?T;S#w7{l6$Su2 zYx$55Ovq;q66=>p&rp;!>Jl++iLmMC!asiX`ERJ|KOKc0#77&fWB%J66 zwWf$|M@LoCmjwfTV1@LJfvGMuNiKPqU?Xh97O|{qyeK_&k7AdO3QX$bF}~+3+a$m5 zUc1-s_5VF%Yin+fXAqH1eq}22x@XJ!*?Sw>?At6?POQk^`wfncy)DA^ew}6b&!&Wt zZ!i>F3wo7y{E|KeS{6${KO;iYA&%7`iQWcbJ(C#CXNcxiNWXC{(fw5-3T1MDpCMxt z8M4%Rj7&vlh-9u3N_duJh85D6nk1{OlO)Rb6AKxk!w7~eU%#&W=$BM;{k0;KeqA^L_oKs=XwJc z(Z)5Weo(cm*SCzqHMXzr!366ln>#y^e$71$Equ!JECYiCG_~t1+r4(LUB>o87+mO# zZzi~bKNsT3&)A-NH!!oes&@GXWG?8CH(Y7ZgqKgK+gsKOGB8tM6D8NT%BG`K1SV03 z@2P0%>UO2&_*M))#Mkv%v8KB3-)g?9&m>3b6+!KANBYzK? z;R75KY01`l$Z#oocXxNM-D{Vzy(sn;Kqr97@9A0O!Qoo&HjL< z`0vDpfiv*GEHF_b`{m%CG{x@j=I*t7?Owt4e+3u--#y?aCm13D00000NkvXXu0mjf D=S2(D literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/favicon.ico b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718bf5dee1bd4a8774615e59b96ba8dcf13a0e31 GIT binary patch literal 1150 zcmZ8gT}V@57(O#`V3do%f^O_8A=03`=)xcr0_!5k@+P61x{y$)Y|gWtotfiCDt`Juu};MvKeDM99f_x28G7e@nR-A!9!=vOpwon-sU^QqZdqxi7P$H zg_9hH)1ja1MT(V?C;rXZGo0^*$8*PNGIyHQnpp#_nzk8?&i3A}67HPS;r$Ik`{rDE ze=?kfeBt@|;|7%~AjNS&QxuR(38188KtW0YArb+`<PfnB}Zqy^XGYza2)nL%5Q+EODK>f8eIS(S?F#D}UgL{Sh8-H^- zj=n&T&x+;`@hGp=XapL_f##1ioG};<25Y(aAMwMyDc4rTM{kn7$K~i!uCLYIie_%s z>w#89vtrcgfV0u|$LD+g|L!@CjnBW%cT}$sgVJy3IA3>nXIYEe?$R6RQL9Zq^l}irK-DUb!&1%VzF2?r6kV82y$%)eeN%Zo7XuWi{?fa>8 zT)I8&+Rkc!__f@5bJ1+Q78y_$&5v5HF12|pSBTf5_3I(u&aw1(WXJRBX2$r>Xic85 rRKTy#E`Zfl@%BdI@b66t{w~fF%c|i1B0LlDF-!wxj89g?^DyimJkHqN literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/gCube_70.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/gCube_70.png new file mode 100644 index 0000000000000000000000000000000000000000..018d52c09c22423b950d764a62aa182fdb8b77cd GIT binary patch literal 8275 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 literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/gcubedatacataloguelogo.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/gcubedatacataloguelogo.png new file mode 100644 index 0000000000000000000000000000000000000000..1b4bf52ba57f072bf11cf4044d691c6f44c8660f GIT binary patch literal 10016 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 literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/groups/icon/placeholder-group.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/groups/icon/placeholder-group.png new file mode 100644 index 0000000000000000000000000000000000000000..1ece1500fa3a7c610346f523cf738d1b4d54f1e5 GIT binary patch literal 1218 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw3=&b&bO2H;>5jgR3=A9lx&I`x0{M)^LGDfr z>(0r%1acITJ%W507^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MS0K<-M)v@p z5LciA|JdR|fC zYzayzZJ#jz*RS9I|NsB~f^`{2p7TlcTtxI2Bp+Gj6b|M>Cq$Io9se*S#^^3Ak)t3#73;&+bx z%mD_aUP+K&FarY*PnlEN|BrX%Eh@Zmo0f@H1C`3E|+75 z@;^T_iY$zh5C-aGO!9Vjark#L<_3_%S>O>_45U54*zIJt9negDPZ!4!kK~N362?fQIORM>*1!Mrv)TC_H>MWP|5dlS^!)o2@2dAl zZnQ8cD4taf&a|BD>V30kj-TrDB`u~04yC$2`P7=^8F+eD-||aaH?5k|)moz`v)xVm zYE-PD%(>}#8egNf3biqt%-Yn(ca^ykc#o8{VFW8Qn@-RqrJP^*45H@BzAdGT8I_T0IauV3H!$-Q`c)$T$r=cSUH z)b-6)uHx&m6IYwfS>3wk(%#?va-VM1rR;hg zdOEW6^VbRTpMRXZoW5t?{ts=hDib=J9@*}%`C8?ae{Y}k^=kbHO_doTU%r1i_l|v{ zUDZLiz@*u#^>e>%Ygc+JzvEz4=492sk+a*K$|_QRefHZlW&i54iz7ZL9KG~3@?p`e z!aPagYbnz%rni3OIsIsjL7L<_zQ_N$Cf7-1uJoAQvc=Yuz1e=!$As4&MQH;3+s-y@ zURbdq*Le=(vF2RnCu_{*a-#Lqe?8kM@y{gfSIy3!f1bYH9#H%LDCA z_riQ(^(&esO+wbOd7Vo|%Y&jFb22>EimdHiytRMJv8mULCeJFpW!iXAPh-n!!IweL z9!y-{T+gWH5N*U7WpxyohEz*jBT7;dOH!?pi&B9UgOP!uxvqhwu90bop^=r5g_W_H rwtUftDnm{r-UW|e%C6p literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/d4sciencelabs.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/d4sciencelabs.png new file mode 100644 index 0000000000000000000000000000000000000000..7d1a75eab5d3c1becaa04e4c53461021fd2cd5a3 GIT binary patch literal 9879 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 YDIlIi<+ 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 T|B@S)00000NkvXXu0mjfNw|i= literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/grsf.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/grsf.png new file mode 100644 index 0000000000000000000000000000000000000000..91ccbbb79da9d3356460272d2267a9ea615425d7 GIT binary patch literal 97630 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 N*IjY<8$a;X{|5cukO2Sy literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/d4sciencelabs.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/d4sciencelabs.png new file mode 100644 index 0000000000000000000000000000000000000000..e0b8d807bd010cb73df9488309faae0803cad057 GIT binary patch literal 8550 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 I@;}Z0J8@2HtpET3 literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/imarine.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/imarine.png new file mode 100644 index 0000000000000000000000000000000000000000..9d7a311f5ee1788f16756d6d4276b91ec1820c99 GIT binary patch literal 58152 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 literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/placeholder-organization.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/icon/placeholder-organization.png new file mode 100644 index 0000000000000000000000000000000000000000..30616f2dd59097bd8c65107cb470b38bef3f23a4 GIT binary patch literal 2969 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 literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/imarine.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/organisations/imarine.png new file mode 100644 index 0000000000000000000000000000000000000000..875348a4561ec7b631cc8636b5070ea33d10715a GIT binary patch literal 106027 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 literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/codelist.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/codelist.png new file mode 100644 index 0000000000000000000000000000000000000000..5cb017a3ed20381b962f8379db4e2e918a549fdb GIT binary patch literal 2341 zcmV+=3EK9FP)r|(7YdZQ zQphx+;Xsgv?EeiQaUp0%;*tu@LJF5et6gkAU0qyUtPMxIL?i{jv~6E}jK`iUr6@#U zm6Rk~YLuWAjWQvmv)k^eJX*Zg9jP`(L~veF>3mS$0#*R_Ay=r!0~`TuM@7l%<3p`% zNP_dX9Lz#dN~Dx)huRP$2qE^NX+?+MdHIUvI;;p@S#&7HOW)f9UIiIt_2t$Oy%Kh* z*tVCK6x=awZB}q@QQ1lXAA?LV`?AO)+9j;7ELl}<#!4~saNgFk%>wEC`o#js1c7ws zZ7thu#>!9+uYCQG-5c-SEntJ?_6;Kv_NZ=kQ~7%Zo^a2G6`Z$u|5#MD6v2XswQx9) z)Zq25uGqPzCDdaf1+RSlki9R?`{@%eb^u9@twUWJs;5KFVQ;*5_fsPlK(N3Y@7*2h zkwFFf9MqFJXG0-iLnucUbV&O`k91D;S@><~xU0Kr|Hy>79FwxwIp-Dtxu#goX zXl2o%6knV*7qVtfN@C&kak`)AC{tBL5=FqrD~k@L1bpCM;HB?vD{*$l#H4_-vkmvm zRue&Ff=`Pco#6Z}2eW+3Me}551`0mcc-Kl*5ktVM`CATVJzl2oVMQsivNj$aZznxD zPFL2{;-Y@Sdc;G)c}1o3t?qheOdJzXcGjbwv9K$1}TzCn!}Xz(NJ2n4Fft_vcS!)}&-Qd%WCe?I2QTj6xy(>SurdgX%jRB~hxvYlYd^9vjka z4?KKSc`@MG@{>)pJ@Al{5KBT_w4wUrVxpLnmBJ4eWs)*FmRoI|Lz?FT<$tzG-(9yx zzzbF?n4XbHLCyqS*~v>S{P*0=fM>gV`nb^4hO?%T&o8$k1UQo8h%(@%Rhz;T$7l){ zOk~D{F}S;WX};TOy1i0xM(w{5u!Pa!>`z_OJ?fh7@qzO^FFv0}{=5vPWh5GU=j^N$ zW@n{v^Iiv~)s0k~YNFHQHDoI^9Ja|z*8LjDG-Tn&x|gPqmZV=&epqpl`|hwuOMq`) zUvaf__*4`BIqRa=tKmqBCnnk!)^=oUH1np6W%Z&=+B1Q_ri1*P45m1SVMirs9_V3Hq$kHSI$pomxX{!_p8+QmjA+&GOml6wJ$D!ZRaH+1_gF zr1V$=6}8vt=r+FOlnTwNkirG#6bE%!26jk7z|2X>6y#*EczUYgwec zEg~UF2wE3KpRQJmi;K1VEzU9o8;uxRT|XP;imRPZs_Oao85iEZ2qyAqlob~jYbt;c z(kT%ejo1aBze5wOX_8}gx7dHIKImCkBoIP6q5n*)+wQ51_QZHFaIWP)e>F`~cP;e% z9SnDOFNaSyQCi(V%l*)2U_->yZTD0{zu=?AYu$N8&LhC8P*x*jqsSPm4#^0pwgZ%qf@e%j3UEBehqI*iax43*>N$0} zC1Ph)#?W>Q{Pi#pR+OwhE`-<@$}ISSyRkXXDFw%ik;T;PtM4Dn$3E9)`=e47&rb*tw>~ z)}bzyuqSe@i!2iM*gDju;qolOWWIF2BYPK`j1uSC3W!}hFMvGDDeJgo7J`5KQu-;HmD&GGAa0;Pb^rr(900000 LNkvXXu0mjfDB^yc literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/dataset.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/dataset.png new file mode 100644 index 0000000000000000000000000000000000000000..8ded500c4c5c6e995cf2fd8f211ae5e8d8ad4a49 GIT binary patch literal 2308 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 O0000QdkK)zEDH8E})!hrgn;00Ar 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 literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/researchobject.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/researchobject.png new file mode 100644 index 0000000000000000000000000000000000000000..cab2b45106e8d66cb1c47e1fd844c21223ad4f3f GIT binary patch literal 2625 zcmV-H3cmG;P)kmi4OU4<(>kN+WE^6Wi8H1XQY^B{!`4?q@ezVT5fvkf0tR`% zFYMmaKUk2*viF?b)#*(7`v>;k?>p!F=G=3>=OL6*JPV|yr3r;ncqD-kK?(IGGW zQaEmxA(epU0Mwm_&)eMvJ646`@Y}B-IKY!n!ZAVJ3208-E<-907t<%5ur*h0)EYAg zu+|pQ{-%q?1ntwa>{3}Rj^$P0adVd~!;Jz3j*A`QhBJrbB_&$L6^=_i>0=saykM)c zmhkEvUwphR8oyaO3$AL>?(qn))-=v|Aujc#j}om_g%h^ssv(NmAXsR(R3qRd3Rv~( zbbR{z7%ZC~070;GgA*)-C}t;Y&9!P4ttxEP8Z(}SvP0paRb%V&5PY;{5h8-T?Hi0> zp;2qhuu3x49*)~(NCl+_?3v$L6tFnN51W(cK<%Vr;<!(e3Ez!-;cukX>}&2FHc8s-OxQ z>70x@BKtikK4rb!HF$T;OZe%O37kS%hgRc+jS0a>TrdqE=iNkM`C|@l>TZN-4}dL0 zpAK8$r8`eer$o5P>OlcONZ>^5S{(rot)pXx(I~BI#9#h?19eTEj2xtoP?KqHGg36j!8%K~A4uqCNR#@20${ss+%5zbcQi`m?d-$y27J7~LL|ejc z07-y(g`?9lwa${J0Sv*rkB1A6{&ulLTtaJj+}wo^zPgUGszyf)C-tg(yw2-WIz#}_ zNa|RbL0LiwQd7dYlpQz|d8}0KwQ3waU5@TvBRi9Is5O#07C@HRFBC}vgFvr^`y(W9 zq9R>)uMy|2*WldsNBF+B4WDk085S*#g{Jl%9Ll{7IM_i^ztLqK9$TACpCr&5)(iym4v zLdRHLsn3LCc@;QyxeB})qxSAT9LX-l#nM_nMov%)uR8wZ`b2c)(teZ_i*#t0sg+^GOj}JNX>^ zZpnzFXUdU#t(uE(Eckf1puVMBk&Y7TE1D|;ituvR4EtTv*oi;bpln%HBR=@*y2H{t z!2zC_>F}LRF zTwzM-;IUTM)P*n3S8@rq=ku%Fr#A~;o;?X#7bo^unZTDhqtR~8JbaR00jbZ#B^Zmc zy!isPlL~Lf%>n?pab#5lP%s(5wys`ubob%kH+h0^Hca)=V(%OCxp-Db2Vt_OD*%81 zY#A1cS`h>_0A?!D-v6N;W%rvLSR?8ea%EH?*2T_3XAirFHPy!rDX+XZ_`9h?Vmk;4 zYD9BUS`ii1&A9!jg;^*iOq$?|?XQPnj@f%-bz3~$G)1Gz;o>Y})3RX1&tp&Y zd5lxzq&}X@0V%C\m#386WFIcwRYmI|CDjRXgD;LHh}i0!Y3VbTOwEgk!3Yn=S0z$SgX z$i0b^48i^Y2qBqy`x6ICEyIsNS=NCF%UO$QJ|3eow4@kGqG%bfK8uzTURggiyz|jmHVh`vN_Sm-tsU2dz)IO`e?BsKA1UBkorvc zFuM#FN^9(_6oS90WdO0doX_oTNQ(&a#>RwTO#6v-lhxcI;aBMe_Dlt!zbmgGJ?V|H z-dp`-#SU=;D42`nmQY;z1aBX?h{OLVWpeWE0>x#qL)>7E#Hzw)4#!I{8k6l+S~j;K z{rb=6@a3hu=(G2ewFHXGFdCDU`Yja?rTl{{>Q$ZMVhS9$SEeq>h+}ybSbyj|E|od1 zct(Nas!nln{=pTBbylthaq~g;C)n7qHWI-Bo_K3nFlP9B*t&;h5c1Ag2b3jncf|Lb zES%tNTpLpW0>p&+V|i2{w*2*L9?n(@tg&Qzta3e%D*!BDzb0Vc6@dR=@~{Oznrl2G j@uRt@WeNYQSMmM}`FI{}nT*Of00000NkvXXu0mjf2w2Yd literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/series.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/series.png new file mode 100644 index 0000000000000000000000000000000000000000..573d43f2de2a1fdcb308b4f3ff72eb50530379ce GIT binary patch literal 1723 zcmV;s21NOZP)S(yledk}q57^7fQF~n&)a4#Fe5Mdj-iPBA{$yysH)dy)S>zuy$r@Apl>YR$i zHrkuq+}|G3)}&3+wDfk^%lhqgCS)h-Z}(d`eUQ)ITN3|gP)ku^eawWFmdf=G8JkDl3u z`8Za$W>mXhx*ww9>yC7b%@PWjU@99vIT44OMQ2djp0g*~x^eZ^gM@h>ZF*Bnky*e6 z3L^Ue?21i|6I@k)u9QgFp|y(pkAt}R*OP>K&qIpVVl_j+j;i`|pU2L<j|5qnqDv@xYBWUnNh1ABVp#+=<+p6ObZKp2ftY@X3Q?kH`X&;b1=gyA=n>|OP`*fhpV?9q$4=05jZWnTrL4Zg&q2AUgnI;H~jeQ``_Zy zzTCAs{Q?BoT`rd(Mz>P6?JiCvm@)H;ykvzy5SEU$Ge$uWmI_Qy3;#QIQH{xOq*iHOVcg*(6%70Ys7&g%hxmVHU|_= zld!g8E}rzs2rDXxfTDsaxOe|)+Jf6}{tY1d=cOf>(IbN32aFdyi2Gz0Zv6fjR*M-w z?5onZGJ;*Z9WFd)3w|H6)+7HpWEJSf~kx_hXERcxh91fbl3z_ zS*Buv$@7v&IiihweX_j9a}U6E0GlSui&{SdSP0;?oC=O*oV|+o2>=HG>%aWPojwwr RqbmRa002ovPDHLkV1khzCLsU- literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/service.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/service.png new file mode 100644 index 0000000000000000000000000000000000000000..54dd8496827d2888fca85f365e3efb19a1e74ae5 GIT binary patch literal 3593 zcmV+k4)*bhP);VE?n-plj3XFYQO(*_ddTpeA)Z#-?WdAQgSbllar&IG)_qqC{v}7Q9>me zK@_t0FMy;9K_!wZsZc4TsFJ8f>4xHhEnBvz_hMdRC<)Kc`M}@DXvmOKEJYzpN%@D3E|A_eKSFM8CVDy?z%%;7=cef-Fhf@`ImRw*O(k??yx zx}WUNU3|;q)}9j1$j)CP;C+w*o(?v&5X}-EKb)Jj-;-8~C&HPV^EU`&-rt@qfD91G zyv)t{8$4;{)(S6q{(!;cZ`vVX%^lD09zw!SeVcyW{%wnmu4G*j&fKsk4849Ig47|K z;k|&Q5R+;7p|@7lITLeAc**kz3}!#m=YRKP`yeUQwd?0OTWP27u*u)FLyK(ia35C8 ziX$@E7XX8oj^`hKfVG(u@X~v9EDS9KsV0BZ4yPiHRycEW{syFcQ|sBbAJ1WYOdyS| zUA(@lh}CoBNlgp~;HU3@&585XcRTgFgAf}Jy`J@!<|8e_7S$v37_b9|dF$!yHevA6 z@zE=b9Ij-+r5gVF)0H!5`5fb2I(_d9X`_OZ73o^ij`BmWG2G-iF@S>MKj+a8Y zIM`fFi4WzCwf$$;>C3hJcm4%CGKwPjLFNQj%^3?ow@GFFZ;x@KwspwqxChZAq?$C) z_O^Wy6(x5t*%7|pjEx*&Enh5eB-twUXt;rl zl-MC4;5np}t1a%U1p`)Cd3mt#r2B#qLRiZeZZy)+(&cgxRh4{rywqAY_L$joveZe7 zdyRf>3Rof)Se$no?E35~Wi>6<^4Zb`w}+^1?BDY59=SqEnReT8(SaxFZT+1!b2PD` z{&q6U(#Eju$Fte^$Q1tbUuRoSc!QUYpFA~#g(q!cvN* zT2fwmh4srOS<7L8!`vPrX;hH)cX*I5udSJGCzFv9Ykg&c{0(eeJ%zdVN0IwXDidOZ zY-Gnp2f97B7Wz2qb8>PNl$0zqEX=u+qL>ojznxMOBM2VuLu0G!_juNzQ@OF#)}Hs$ zD~yR4Zc~>W7eaEJcHA#6G60Gz@bmE^Jt=~T_XTtC>vArYH@Tb(018<-IXQ}0_|m>~ zfjFTh6C3K!_UGo>Y~@eR+~gM@e&e+71(Rdg_{bE%qQ?Gmq7)x*Ju?%-DY{z6p`x3t znmv{uESlinWP8zJG~umR(CaK`t*T1a|MnQ)RkyfkX@Nw}*2T|xVg;~ROQ5Brn>{B> zIdP$ybx7k{kF*TM+_XTtO zd^JT^Z{ej^2o3OIVM;6$M+e)qpOq9zY^d$l)p9({s-(WTle*iT+-mMZuM?ysgyQex zJVxk==8!^0>3cH*PN67*(P6fejvDF5qu-0?(eK6E$#->|Q51ot| z(vnhgsOTo|d~(_DY|tr!q|w1Fm>k36>7(r0RyTHV`f{xcn*9(e(Ymp79$OELaV97w zw0D^pIl_m7Mdei7Y9}GuvB$J+?>T#G-AUoMwUpPjF@1a(Ub_C>;iXf!UeiKFy_*h-pkCjb>v4&F1!e3jC<7qC z&znWlEGnhbXyWYy7x{jAoK@GBl-1L1QmrRwhtXt5wx+3rSKc{ITW613b3JyhitDv4 zys7O*PXApw5)J09;Qc(J!TBvI{baS`1PI=z9^~U z*!gOF4SEiKRnD1f^(cxUH8FyD$x*DFJ(iddKWqCQlS=mg6jIaF;mNQxw|CRjYGn4L zNSk^eZynJgexyza=kS@EHs_5E1O_{Q*cBd|pMc4v_P%pQo_uILWi>6-w{)>>Z;4ed zYsP4vTXw%@AMYK#!nLYqPlpEJv$K^fpEbtLFahv2=!ppQrK#0%v{v7nvK;TuU`eBb zd3s?Y04wK=C8#$T@Wg}T__Vl!Zj+hP>K4qZEWnZ(qggUzGyog^aEg-B25N72FkzJa zDpb_BQ+bzBkW1xFsH(Kl$)_*Z@~e-|((3F?5LEW9uv5&rKa!Xb%YoF~-p#Jhuku}0 zGd*UNl=x5{o;8LMzTN;Ry@%7U_mnWg*PE1hTTkcARi|-sw_sK!jjcwTaMI3Cuh7)y zBJm)pQlvtqGZ`T)rz}8OO$%#(b(B3PODQR9;8J-L?;X9ubK8$mUT4`#FRXZg`N`3A z8cn?N&S^>p83sDt(8R58HFw&Sm!{o^aItVwp;8f2IDDbTM2ukP#)lXe9f(<#yteZz zO*K+?yOZDSJqti&upgTqO(7=4ACp<-=-CPz+0X#@JuXj>krK;>)l<+Zj#&#SsuYP@ zPL z(kncZk?687XPBQHtywQ694Wr3-AE;W%a$#w9zY10ClOB%mPv>XWLj@LY4X?*!UBg; zce``27yvOL{#N_CsntkXZL6K?=);4PWBAL-(mOIfmZgnhW^Wz~EI#qxf-~W9`1xv%ovWg)tN*YQ0$yA-*)B`r3KK^M^X!r&o4T8~+W7rnFYs|e znM1OGv6o|i<{>jXFAvDll3q1uEbDq>)1}od?E3sFC1nlx8T2HL4ra}QafAo?S_!|n zHs`g%!y*bo$4A4A>g4Yv8-F1M1Y^S&9ht0oveBDQ(WZr^AF`N zwZydg>(s5FXc8W1o5n?sU~}(k4n%!(7doBb+lt$oBWkI$06%YjwlM_Lg825Pv)GopweOd0ZfSG=Zmm04z6QPJWE6sosj=2L zagRy09!xL%?u65T+K-k@WaVtfDg&UctA}%?4H&$2ga!;FEO40SD>N8?JamcOUua+5 z5<+}%@b#=m`pUN6lB#ASl!piBmpY6ltHEE>)WMjD;T$fy$=-r8o?e(pTW62U=waNT z2&ESMeY{-epo~3cdQ2+*!z@!YtCHgmoe|N4s%G2N49*W{Was~vK(2Q-s1Ue%XY4R% zQY0@wKFwP0vwH_RA+DgRk{5TJ;&R1pGN;Cp5FN-qCx?|pY(Jcv+BAC~znt5*fF*irkPKeuC2K#dt-;(e+Fyz;FOG+Df zd;dkwl{L`!kJj>E_n*GOOMfcl`k=h>Wd{eM22=@;Yh6v$bX^YTX6;9aP44!7H@I-4 z(Wbnwpp4(_E#b_yTm15)Z|Lmx)Y>|GIQmV6Ru}dVVw0mQiB9inWn}05`Tyhg-K?Yt z(k4VuQQyYyW7llXhp3T$M2+;Lw7SKu6pw_RhjW)c>FBxBn3ldxzs~GuO1Re-(@tHi z;nc+%&1Y2&?e5>Rq;4J{<>)V1s9xw>Zw9^RMky`A?nJiK;%51}H&CP%mKwD8Xh;;wKwH*3ER zU4lex2jO0xWJn=;Bx1V`U4o}%;h!IQ+!_91KI0jTf0z%owD9-(Dc=79_QbDHcPL4R P00000NkvXXu0mjfY5N-p literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/trainingmaterial.png b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/images/types/icon/trainingmaterial.png new file mode 100644 index 0000000000000000000000000000000000000000..38ea713bfb7dc38fab7e2827ee647e02d1acc668 GIT binary patch literal 3612 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 #bYcaBjii#NBv5#f^>&lzeFS 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 ZZ{PgRvdjUidC3z#dh(*XK61{#{eN06`WXNK literal 0 HcmV?d00001 diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/jquery.tagcloud.js b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/jquery.tagcloud.js new file mode 100644 index 0000000..4e5d5a3 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/jquery.tagcloud.js @@ -0,0 +1,92 @@ +/*! + * jquery.tagcloud.js + * A Simple Tag Cloud Plugin for JQuery + * + * https://github.com/addywaddy/jquery.tagcloud.js + * created by Adam Groves + */ +(function($) { + + /*global jQuery*/ + "use strict"; + + var compareWeights = function(a, b) + { + return a - b; + }; + + // Converts hex to an RGB array + var toRGB = function(code) { + if (code.length === 4) { + code = code.replace(/(\w)(\w)(\w)/gi, "\$1\$1\$2\$2\$3\$3"); + } + var hex = /(\w{2})(\w{2})(\w{2})/.exec(code); + return [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)]; + }; + + // Converts an RGB array to hex + var toHex = function(ary) { + return "#" + jQuery.map(ary, function(i) { + var hex = i.toString(16); + hex = (hex.length === 1) ? "0" + hex : hex; + return hex; + }).join(""); + }; + + var colorIncrement = function(color, range) { + return jQuery.map(toRGB(color.end), function(n, i) { + return (n - toRGB(color.start)[i])/range; + }); + }; + + var tagColor = function(color, increment, weighting) { + var rgb = jQuery.map(toRGB(color.start), function(n, i) { + var ref = Math.round(n + (increment[i] * weighting)); + if (ref > 255) { + ref = 255; + } else { + if (ref < 0) { + ref = 0; + } + } + return ref; + }); + return toHex(rgb); + }; + + $.fn.tagcloud = function(options) { + + var opts = $.extend({}, $.fn.tagcloud.defaults, options); + var tagWeights = this.map(function(){ + return $(this).attr("rel"); + }); + tagWeights = jQuery.makeArray(tagWeights).sort(compareWeights); + var lowest = tagWeights[0]; + var highest = tagWeights.pop(); + var range = highest - lowest; + if(range === 0) {range = 1;} + // Sizes + var fontIncr, colorIncr; + if (opts.size) { + fontIncr = (opts.size.end - opts.size.start)/range; + } + // Colors + if (opts.color) { + colorIncr = colorIncrement (opts.color, range); + } + return this.each(function() { + var weighting = $(this).attr("rel") - lowest; + if (opts.size) { + $(this).css({"font-size": opts.size.start + (weighting * fontIncr) + opts.size.unit}); + } + if (opts.color) { + $(this).css({"color": tagColor(opts.color, colorIncr, weighting)}); + } + }); + }; + + $.fn.tagcloud.defaults = { + size: {start: 14, end: 18, unit: "pt"} + }; + +})(jQuery); diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/location_to_bboxes.ini b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/location_to_bboxes.ini new file mode 100644 index 0000000..e7f14c2 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/location_to_bboxes.ini @@ -0,0 +1,16 @@ +[location_to_bboxes_epsg_4326] +Antarctica=-180,-90,180,-63.2706604895 +Brazil=-73.9872354804,-33.7683777809,-34.7299934555,5.24448639569 +France=-54.5247541978,2.05338918702,9.56001631027,51.1485061713 +Greece=20.1500159034,34.9199876979,26.6041955909,41.8269046087 +Italy=6.7499552751,36.619987291,18.4802470232,47.1153931748 +Ivory Coast=-8.60288021487,4.33828847902,-2.56218950033,10.5240607772 +Mexico=-117.12776,14.5388286402,-86.811982388,32.72083 +Nigeria=2.69170169436,4.24059418377,14.5771777686,13.8659239771 +Philippines=117.17427453,5.58100332277,126.537423944,18.5052273625 +Portugal=-9.52657060387,36.838268541,-6.3890876937,42.280468655 +Puerto Rico=-67.2424275377,17.946553453,-65.5910037909,18.5206011011 +Slovenia=13.6981099789,45.4523163926,16.5648083839,46.8523859727 +Thailand=97.3758964376,5.69138418215,105.589038527,20.4178496363 +United Kingdom=-7.57216793459,49.959999905,1.68153079591,58.6350001085 +United States=-171.791110603,18.91619,-66.96466,71.3577635769 \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/oai2_style.xsl b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/oai2_style.xsl new file mode 100644 index 0000000..2a41373 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/oai2_style.xsl @@ -0,0 +1,690 @@ + + + + + + + + + + + + + + + +td.value { + vertical-align: top; + padding-left: 1em; + padding: 3px; +} +td.key { + background-color: #e0e0ff; + padding: 3px; + text-align: right; + border: 1px solid #c0c0c0; + white-space: nowrap; + font-weight: bold; + vertical-align: top; +} +.dcdata td.key { + background-color: #ffffe0; +} +body { + margin: 1em 2em 1em 2em; +} +h1, h2, h3 { + font-family: sans-serif; + clear: left; +} +h1 { + padding-bottom: 2px; + margin-bottom: 0px; +} +h2 { + margin-bottom: 0.5em; +} +h3 { + margin-bottom: 0.3em; + font-size: medium; +} + +h5 { + color: gray; + font-size: 12px; + margin: 0; + padding-top: 5px; +} + +.about-xsl { + font-size: 12px; + font-family: "Arial"; + color: gray; +} + +.link { + border: 1px outset #88f; + background-color: #c0c0ff; + padding: 1px 4px 1px 4px; + font-size: 80%; + text-decoration: none; + font-weight: bold; + font-family: sans-serif; + color: black; +} +.link:hover { + color: red; +} +.link:active { + color: red; + border: 1px inset #88f; + background-color: #a0a0df; +} +.oaiRecord, .oaiRecordTitle { + background-color: #f0f0ff; + border-style: solid; + border-color: #d0d0d0; +} +h2.oaiRecordTitle { + background-color: #e0e0ff; + font-size: medium; + font-weight: bold; + padding: 10px; + border-width: 2px 2px 0px 2px; + margin: 0px; +} +.oaiRecord { + margin-bottom: 3em; + border-width: 2px; + padding: 10px; +} + +.results { + margin-bottom: 1.5em; +} +ul.quicklinks { + margin-top: 3px; + padding: 4px; + text-align: left; + border-bottom: 1px solid #ccc; + border-top: 1px solid #ccc; + clear: left; + list-style-type: none; +} +ul.quicklinks li { + font-size: 80%; + #display: inline; + list-stlye: none; + font-family: sans-serif; + padding-left: 12px; +} + +.oai-footer{ + border-top: 1px solid #ccc; +} + +p.intro { + font-size: 12px; + font-family: "Arial"; + color: gray; +} + + + + + + + + + OAI 2.0 Request Results + + + +

    OAI 2.0 Request Results

    +
    + + + + + + + + +
    OAI-PMH Requests:
    + +
    + + + + + + + + + + + + + + +
    Datestamp of response
    Request URL
    + + + +

    OAI Error(s)

    +

    The request could not be completed due to the following error or errors.

    +
    + +
    +
    + +

    Request was of type:

    +
    + + + + + + +
    +
    +
    +
    + + + + + + + + +
    Error Code
    +

    +
    + + + + + + + + + + + + + + + + + + +
    Repository Name
    Base URL
    Protocol Version
    Earliest Datestamp
    Deleted Record Policy
    Granularity
    + + +
    + + + Admin Email + + + + + + +

    Unsupported Description Type

    +

    The XSL currently does not support this type of description.

    +
    + +
    +
    + + + + + +

    OAI-Identifier

    + + + + + + + + + +
    Scheme
    Repository Identifier
    Delimiter
    Sample OAI Identifier
    +
    + + + + + +

    EPrints Description

    +

    Content

    + + +

    Submission Policy

    + +
    +

    Metadata Policy

    + +

    Data Policy

    + + +

    Content

    + +
    + +
    + + + +

    +
    + +
    +
    +
    + + +

    Comment

    +
    +
    + + + + + +

    Friends

    +
      + +
    +
    + + +
  • + +Identify
  • +
    + + + + + +

    Branding

    + + +
    + + +

    Icon

    + + + {br:title} + + + {br:title} + + +
    + + +

    Metadata Rendering Rule

    + + + + + + + +
    URL
    Namespace
    Mime Type
    +
    + + + + + + +

    Gateway Information

    + + + + + + + + + + + + + + +
    Source
    Description
    URL
    Notes
    +
    + + + Admin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    Set

    + + + + +
    setName
    +
    + + + + + + +

    This is a list of metadata formats available for the record "". Use these links to view the metadata:

    +
    + +

    This is a list of metadata formats available from this archive.

    +
    +
    + +
    + + +

    Metadata Format

    + + + + + + + +
    metadataPrefix
    metadataNamespace
    schema
    +
    + + + + + + + + +

    OAI Record:

    +
    + + + +
    +
    + + +

    OAI Record Header

    + + + + + + +
    OAI Identifier + + oai_dc + rdf +
    Datestamp
    + +

    This record has been deleted.

    +
    +
    + + + +

    "about" part of record container not supported by the XSL

    +
    + + +   + + + + + + + + + + setSpec + + Identifiers + Records + + + + + + + + +

    There are more results.

    + + + +
    resumptionToken: + +Resume
    +
    + + + + +

    Unknown Metadata Format

    +
    + +
    +
    + + + + +
    +

    Dublin Core Metadata (oai_dc)

    + + +
    +
    +
    + + +Title + + +Author or Creator + + +Subject and Keywords + + +Description + + +Publisher + + +Other Contributor + + +Date + + +Resource Type + + +Format + + +Resource Identifier + + +Source + + +Language + + +Relation + + + + + URL + URL not shown as it is very long. + + + + + + + + + + + + + +Coverage + + +Rights Management + + + + +
    + <></> +
    +
    + + + + + ="" + + + +.xmlSource { + font-size: 70%; + border: solid #c0c0a0 1px; + background-color: #ffffe0; + padding: 2em 2em 2em 0em; +} +.xmlBlock { + padding-left: 2em; +} +.xmlTagName { + color: #800000; + font-weight: bold; +} +.xmlAttrName { + font-weight: bold; +} +.xmlAttrValue { + color: #0000c0; +} + + + + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/pageloading.gif b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/public/pageloading.gif new file mode 100644 index 0000000000000000000000000000000000000000..ea92a4c133e147f8f7b3be22fb5cc14944942305 GIT binary patch literal 100082 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

    mfq834kb#e8LSx zChce10g>D^kSO8bO!MVZ;YbGOgrMD|^paDwy&>0}^QG%;{;f{KG3sG+^RqW(k{us$ zIrLUL_HBW9DIoR*z_By_CLMf9KSGH>64EEb7fwsZN5l2WLIjeMKKK$Hyc(8lM7Qx z&34HbfC%(4S>O=FO^=XtpLTo}{ntI(b|Kn{7%52yb03lgj>%{s;=(T3Y|>!?&!^f7 z7K?Fejs_#b8B-bds&uD$si<$al|x&hhpU96yt!}|2dvD>SIe6x>@sWYVjwAZ(gVSy zd!z_0F*240@)A=1j}mS ze*os3)PkDI!Ux`J9~%ph}I@ zqi)$X-!aH{k@Pjco?>q}5X&+9lJ(!1&);G--mcWS;~M13;k)Hg0}8bQiM1|Et-}P4 z0Tsr!Z^@SAv8%W*Ln6o?DdcDvNE!)jWu-;Q3t61pj&WPcaJy9E=AOU(Psa%jCv&%w zrES1PuW)wHwlEaL1Ejm}_wBp+x8b_Gdt9qGt1T`Ey1UIp7*dyRDBe*xwf1k24^y^7 zKJM}blZ}X=7gl7+VKCQuq1zOa<^oAtDH5ZK-Av0Pd~vtQ>q#3XdYk z+^+G(!h9(cM()uyHUa1N$?gER5@~_Op=ofbImj#7_f|>`xlPyeh1Gj+9EhhC#1jW% z!2{^sg|Kp%8;Qia0A?BXVG%pP&^%8vUV9EW9YlQebjaM>3tMJ&kMN)!L^1$As(WR! zTawc|SJzup=RK$Yx)(Ql&BTzs2|mV*C~dJAld=dF_kWisp9Oj}5FGP*6)af>{yF!d zR~^#V<(Xl8Fc7|X&51GGyv_>fZ}oKo~e?0oEaz3*-F;m|nh z$T*6*l*}RPk*-Q75r$Zq;!_Eg}J(5D-xzg)QbMd9Wb#mf+-)Gx}l z{EM0X+>bC> zsMGQNTv{|nX=@_LOCog}F8jgQ`n}gs5Ywm6=pmxHEbPKne+6rh-|72GLnQx}FITL8 z@csBAFdrD4`s1{TN56VNgrd8Po4wxspAB?~F@9^J#4Re-OY+fS^u)e5cc5(dq1Gkn$fP8bADSL!YW#_ny?= z8QSJVwz~EYp#qM96CQ%A5OLcw{EtUzP~+7PUfrRwtan=;g}kmkZetCzzWT#v=UDC2 zaVqNrryqVdyWdw@d+Q7BhVF1e^+~rU{_C-ckulwHf43WDecX{kY~l`#OAX#m4QWG! zT^v5Kz6uRMAf>?O&*bDs8D$T@wFi{19~Qt_>_vQ)ZyjBa#eCkJ+B@EzB2muNQabBF z5z-P8*7e?dpU=H|cEN4B%HzxV*H*zU!>yZN`s3L(qh;(Y`rMQGWSzX1HrLD1>>~9& ztQ|G7}hEiwj6w0Sx7!~N|Wq*J(8&@cE8P3dl zCEa*Fn0BWCTx}=QzU&frrRAeWG&8Rqd0}3bk;r@bVW>9Zh$KlMzAE(oPv!eT>hnO5 z%FM;xVp-2tB9o;AbA`1f_wv#=FY(0&uMMlQ%*5rlkiAm^8N?Z)6{mGH9Qqw*qZ7$C z$R-#oY!xTFbWKS(dWmfkfNODm<8?QbzaxY)GqcRFtT;FQ*6VOY*WT7`(G4C>9Jpla z_#aAdg%m3IELUet{+LZ4qD0rb#l`nPBu#N@9XOxs7dI1J^f;3RC1+#HB^rNwPsmy# zLeAdSwLIhGRMJo9f}2dx+2ePbRto7wSlHBiBeFWze*{ZU@K55x!}Cgf^M)BDWzi8PEuGU z+JY^=;D(vJLd4SVVb&WDn9(3rC?Vh)l!!w^w zi*bLLP~V&pa&tg&IkYPUB^B;Kkn_&AX-zck^p)wD_xonm*u#GC^~!~jv=8faFa%q)PL3q4$7aH7}Jm1u;b)meXGN{&>x~5%{Djf>{`CvpC7E}y;}5L;?ujsw4Tmw1?#$EmG(Y z4yh&9%a%rqI873w5y|(Ek@7y2L9hi}X4d>(bnd=q;(Q>x7;NS@o9SJ58f==OrcD!K#t< zu)b!F%D-g|X8kJ|zegD~4VM&+6mVZmUYyw-NdCP#Pcz}k<_jY>2T~64J!d0X{4^s( zl&#kUC^-BLh;sceq?fE-=WvW*?C@i`Yj)J(7*J}I?XDDhD6Wc|2irdXa9m?qEJnuejw4?U4jKMJiYoy2ntK_07SW4zbsuNMw~u#9Q!ah}mj3%MF6C#+el z8NCDCu+9rCuPe#(Y6$xw7`!=f1Uv|f8jA9@l%k+$V zr{74^--`e1+~yKk>c@LFVud{K7|FW+tVlHft9(B;?K@}~i%*|IskfS*-??FzE*Fcv z7WH^1KQv*tG+wNPXEk7k>tu9`jFONjiNt(728tgWFAdd(F;B;eM&%Zv(CFY_d#1bk zln8-qNZ}NvlKkw7ig57K=}cQ=ZYUWxtx{B&^tNn2>m~U2KJLH?Knm;qVMHLGNdJB! zDIij>EBEG%For$YF6Vt*ib`UD zr1*GDKN}h4Vl{*e#Ogjbp8WlN;Z>Q-l`lP)wDbgDH>l_C7T(%!mn`n8xDqfVc-9Ch ztfC4es`;t2yt()kd9v1mwXA*hsT*`xO|Iu+(eMW~sc`hig5_vir?!;Bkh*35o7A6E zpI%q@?FezoCER}4&_9MIm12S!k+&RM^UGr5lqv~Yh<#G(ekWt|R<^z0ByU4Vz>0s! zUuEIs;K^S2-HeoJ6?0@=-#ddU-@yIg^)_1+B+ zI2wBNc6bLb29Up;(=elB{+OJ!e4FKlxP=R6v)iu{g86c)x~}}Vz#Dr zWIQ+^=iSZ{P|SA-dV~`nI`^5Ls=8V5=<_DZ^% zB?6s7-y7#I$)8Gcwc%R^_s!A;Z=|?*S6UoO1#75G`-L0yZyL-#!MDM4#Mrs^bE}S}SYV4^ zm4JeX8D^k`@yec*$TQcSnP^gA)K9Vw=ij^0)?dzm?*AN)EEfxXQha-JRPUAUUYvFI zt7qF%N+&}qe14I^QLYNtP)SH{AcsYR#Md=qN<39S5mhag&Y0A6@MH-(_`@M`)+0OD z@}8UfS@V=R#q!Hm6#qQQbj>Btp^S<`WtUqx*t~Ky2A`#`(GzveA3|E_@MA3yrupL=fZ*el9P@Jae;Z zOZ#W^7%AE;NI~K8l@~>H0j4MUScm1UuOlYMJvAXoMtZrs@%KF&dIg98u~SRdj+mgIV~dPe6cQi z`^4EC-A9tLy}xWa`gx<^e7d{o-94R_uD~Sfi%OBYFHvWqATu{f?q8FaG`_caWm)=^ zW55!VRii<0Mjlyki4kE-Xh74%Ah*wsEsy;Ap5WPZN5#MGb z%X-l2pvXW91Aa-Y2ZT9{4OO~^^aCO$UN_F)u2FoEKSL0MT`^ts<9+Wx7`O4ApwL_<&D>557#2Zfi>`>JL0iLduyyM zsx#0>Kk3nrq0$2J+;YR9*F#~ac;K2%{uoM=bitk;g!&d&jk)LN{7GZ;BzAr{I5lTV|rbq)n>3IB5q zgHG<>v%pXF#lu+c5up8|PDhFJ(*3LG{pY``VODaa2a;!=Yo+6$&ruPgOLCxlrX&Fl z_ejwQ9ELxN3XLU!b7j*=80-cz*raXrXLukX>F@7H+%YF|2d2=gb}?MyVk9=ZcMt0l z1;4w_`?Rha_G+CXqg$v*jP6wA1=ar$$_$7M31-cJzoelXNLp!3-1M(l^F^dQ?U0f;rlxeKRxxo^MiO_ z(H1{+^%CVW>k-<93crR$Fi%3nCxJR5TiqleJ_#Al^s+fbv-dD<5x~+^NMzGJK=k2T zM7W$ALNqSIj0S&25?F&coYJ{a1s6(zfg$;D8?xHeZ~TF##eAz^~1-H34v0 z0>qdO$S~k+8t5@R_!>Tty5k|!5?y?9nJ9W3?c|QQI|*qda79z$(zJN@h42f*F_(rt zrB4~_QbAZU02-}By&1xyi4ZO-BxxZo)h6lF!ecW^`X9<86L}yFj;2k5&9YM6 zcOQO0JbF79tb2%BXvKmU;4CVfaL6I}3?;)zU@idNkxM_f8|N4op@&MnruYl><(wcKWSs{4-+3`=o@ja(Ld4Nx?N6p3>|O(BYtxhlUv=Cp!lzH%Rl z@>PlQJ93;L0+P;D6=YCloL&0QiqAt&M`$2=@CAotSPve7HbZjZpD+I^JO zT`D1&!Zdn7U;=GR0+I8u=DA(o{32NPkP18bM|KdZ_11wqR503^^=EtS8T zHd=1(9<2}LYr4-=5hfn{|JV+bFEALR31x=Jlq(jNERV8p_Tht`lH?qwp&n6z{|FEDO6(mu@NG1$WVz%l9iX`_}ce1*=eLuwy|Yrlw>!wkewn4&DfU`S+g~0k*!gYYz>C&hGgG&*$WBf zntuQ5KDZzEeQ=#!=d&Hn_w#taU(e4ADR2*922k;gI#6;heF5W{ZA9zZTaFpH>V1w6 ze$Z?p{nmJ6AAeJ_Yy+L=>#Rgz5eNNIVr+whTB`x{ctqR1S`IWgaT|aoH(wxtN~vUp z^?C+S%MX(#&U%C#xeP|r9@|%G+Jjyj#0=2In9#O62&vp=883ENGLvXBPc|CGw2ovx zZtUg%{B!&rFBK|m3KvJC`t2^l(5Sd<;2au!DG|0i2N>43skneOsg!tdtAaqQ)p7mX zwGVV_NV>p(@}o2XOOqciA;2drjDt$YKarDqQrTu?$_~RpM1hW{sMdhv<}KQEb-g;< z7~16WkA6fQUWHV`kCTfKmPpA*KxcEij45wgKZA1_8>>u5YA%o8ar1_3OZFp3wL74R zg%mmfr)QuXv(n|=ozfEwBnS20fR2kIJ9>H#3w{jy08s3kVF zKI%?11$Cmj9;4V3`5_+EUaESB-#R57^;vPEMjiJa;sTz-6iu4~H2iUU9PsLHPX%|a zcu|#_z?w6?8{NIkOnueI6k0~UWgCgqMxCYhAw;1RfEAk8-Rafa(cPw~4dNqHH`l3K zay?aE9SkJol<7b4ad0xxM-w0QqdD!5D9h>t8j}oP0QSah2!x1$UFTIw z_CR|wbn#6;f&jjNZ-Hr3A9+#5!l=uky^3Hs#c$BSd(eofN495Ra2+yg0;fR_tH%Hw z1YuC5Itvbngbh0j4l49BnCcAS9f#a{dSx|wS}=ELyyKXEm~Q9ud3m7x`0J^>;gIs~ z&`GvN5)y);CkT#2PEu3rk=`2+QSu0q6(VxjlGgJzeRAv!fsTTqe>XXruQ9?b4=OPI zCpjM99)r^)2dNeAO%qOpbRciAXxo#JbXOSG0FCL5enLb ze*ZJ^eq%IZ16pSSD3TDnj{#2rO7D+4(wJ%wnkUL4Ag06AP*U21n4J| znalw`ER4ZrcG+Qe&TRI0a$2UJ;T#?!Xig7loE6BQ$`PET3O2&EJ0+6mQfN5fFa}}-~3*byopd`_>csA0k*~(w}%_IpC_%23w;;Y20g(ce~0fJ_ip`3f+&z9 zAv=ub+apMq56b;e?=fg+BbXo9I8U=0py&aiEr6CoAN#vGuKAM~&en#9^GyGoX~Z2R zZIP4UvC@E`0IRq&yMzT;$^vkqzM?^mxktdcx7#Xbw#J=)=7+N}dhIYS?P)&T(Pr7D zfZ%imh%@+IZ@V9o&VYpl*zzI!8T=%2cGK?R-ub@mv!C}QDjBDJka*z(I?#Tk@IIyw zx~c~JDBtHG@0)JzW1q0^mlr8N-+1tFuO5++-DG-K%aZY7MdzsIPv+;pD~P|_iNArq-<$+c25{JX=78e6p|=N;et%-w0p1=BvFxy&VY_xhhHBuD(QR|gTZ881Ch7i;Hx?n>V zdpKk5Dr-!0GFs!@WscEzA|{z-4m~eyzjB_vasJLyeMje_1rbBgVSUl5;jqovB-P;)bLGgpYc`eFPO;rZ?ku(la+hC4 zY_j`mR?r5y`Yn8_^$2Zx(k1OcNDI%h#X&{F;p{@F;N_lN&9F1CBa%|De@TAH@7P(C z+aAE}DnG4G9kQkYt$l_+uQtijB%9pVg?qCcDY`<9F0*<1?xR+&pBwnv5`tB9iZJTc z6%4cLDmp6)l{N}B6{oK!kH0~;_|0`RKcl+%TyStFE$9M8Yuc365Ili9g?K>t1mAn$ zj@&OM>1#45YnQ#BLb|#%4jy#*euh{aCWzQql=Rh=|M)6rFddF>J`IV{O{yU?s4??G zl)E2s^XQ=bDXiMCRK>Ff#|vC3%pC-M2{^+>dxY|RDOHLc%EX9!lHrFzjo;cGqM${< z8+}TEDy&lm`u^m$4x@bcWfKajxXHv=mg#-B?lJHNCOkq(AeJKKin(>*0oPNXYUY#@ zOG9>dzG-Y6UJ-#yBVDM<>^}=&Qja}%ui+ksaSW7n+D6ft1Y@j6ft2#WE=xOBX&ATq zY4^y>nJ;kX2sXq@<%o%PF2j*qc|8*WdmJm~4qAIX(f0OYpgi1~_aORqe0c)&@>4VG z(VJjI|0oOil68KD7rA+iKt2YU#3$4#EB!V4K#)MVwiD1o+>QZ#ds){9gd$SE#qQPW z8=x9L%6)Y1QgeEG6Cp#L_1n6Gy?2M(75RI0KVmy?9nSZu^rrUq z7+oskwmAM#=SznAzDd?sPz(*Cc)p(8Z|0VM@lrX+g{L7FcYD>9VeFOId`Dt$`lZ^G zpzzx-Z+4CR+<(X9k*RFuJn59{c&0esq&ZW2f4f8YB=qOklUw&vB(fHN`J5LOg7G)qti9KpfVPrQXQXy znK;E|Gtu|hJ+xmhpBs~sR>@5wvI5L{(ZYYl<4TII3&>j;mkCyiFclL-jt-2kwXtQ8 zc5z2j9|gD)&#cl9?rO?+UI^~H;?tMJ78)aV z7~E?M;mBLkTfV^Uk8O|F3)sHlu6yR*r*c4EsaV%mPA;VXlI@2ao|mTtc(Ug$b_m5M$l+_HN8YEbG@%B*EDf()1ZBWe6NWH8ld z&Pc3()Fkh)cWm2T_e!ah=GN$yi~DJ>F=5LJ?+8|;{11r4Vr^TN+V>Gs>u)^Mi7ao~ zjO3rKOZwNlJ6^E6Ry*}Rp=3}W)qZ%b@>N>d>zEOr>$mBG>J{xq4o&y2Z$KIqC#n z>%d%3yG^0asVePRYrMd(5ItJvlypl_&c#H{ZZK#*88Vx8-SNmSW&QiRXRW2Twixhz z#sDu5&)RLvqw)SmVi`}dZuSt&exY^YS^a!%Pyxnv3Wk`Rt1mGjSf|D7pV%5*83-T(4o@nzv;Gb}n zGB_x#@LPfS5M6{%RNDooeq;!{WQw!f$Qw@qrg}PfPrdtG%t1SMY}auW&F^)EHqptS zCT^;y&U_wSiFn0umI0A$YwyCfYr%tN$P_rmht1b7;%B<4tTx>ANuy8^gL6}7wEe0* zbVvE!EX1M|FDOO>79OH2h1hDU)_=ev)fTNf^zKz);;3ojcjZBztL-21Pu4s|Q!yjr z+vn(Umf z1J1R=wlL=Z*({;43$5ERRpn+I9&Sftqt<067s#v5EcP>P3MwZkl+QN*1P-Jm5$7=b zXFE#X)ajt@UZdxRyVsfE=bPs+D2$m`Ku*rcsOmuzd%j;}>C7PfcQiW$U|vI<>VVea_aF4(%?4}j&%2fq!_CrIZ{wwZ7=pp;#3v!NX{obu+CF(t;bIWyzK|Q zlRB|*(=B9{@okW~xQ@^VtQimh{&jD;@TL51{i!YUd-ZOP%YVXeUrJN;^o*4aCn zrCpzoziQOatG_;rzie}HN0z*x+qicf!)E&VvU^0v2qZiib9z2j{vh6+40kAo^X4}3 zeju;ESCg@qg`lAnwT7RJ&Aa?fwAb^fHY>E3i%~&ba*9k+5zo;vd>^#OQ?&&s^%b6F zvWA-GSPi&%vzWThzFISn6dZOcj>jfaLXLDk4t(-GrWY5@bAx`WhY zVoj+LS3>3G+oP1kk&vwDYiZh-?&@C()iZdZ=3p2zY!@RssDEmqmFIw>b*zttfdMMW z(LMg7T3g%Gx~$~3+i7jqQTnnUv>mc2RS9r8sU}Cc7N=v1v$#RP4KOE}(8qws@qhwv zl!B2lnRn>5sPKDt4cyC`ZojysWutPksf*`XY13TSv)I+g-vDzV+XHQ42I`yQMiuG zX1oCD3n2^cSln51x3%<2-}SuF=dhtxhoTLzrP9N5{YLo1qD3G33oUUeMj} zHL65#1@xI@9G7eK>_tcRhYq{#+Sf9$aFl_+z?T#`<05tADl3x*#ivt55WHC?F;Bs+ z2lSBQXa};%v)qpQQ2qD?V-Wg`7KyHz!vIhNajz=*m5J21>>|>-Btfh^3fzeX}AAd6_HBMChtUU1CFyW}6S+mJj)AcHC{cF=)E^ zp~D_9uRe=Vw4qzj>w0R{yJ%%mb)#>8A%bb-wUeLr;UFKURkg}M^f3kdr#rf5p|AID zYx*fPJv#EMobfk3D$Ltl$z_` ziITVG!F!7%ejudSPx4JCpMpo=z_4Q^gG3bQcI8+0%O%O{(s&_oH3HI9=eN+1_O4A_J+`#SFHmD2bo%_ zS2@pVlJOjQ-;}Ga+P6YzZ+X}n!GR{okYk_IObJlw&-tGzS-E1!bM>-@^^L&cn~Rnw zsOX`ekVu=rLFGUTmBnG>mxCN2n}_GYsZ|j-iRb=_5<{B^nkd173$QWg@FUsht#k8i z0?&U#T}4=%RoIh3oZbV!9u4Y(Mw3t5vLkI$&yO-2QZf#rAnvxAfBvBo;&x>AVs!bU zU7UvX<(os77cJk4E2-lLCBypczga(pkG%A@W9NZ?f=3SBiKn@eD7ZDvm6T0h_Arce zG>ps^oOqi)f|<0%wD#EtC|j>Ya7BN$XBvGNXkQ~3T~kgcOFLL|(sN2d{qwMNQqX5` zhP$L_@?^B*j9qiX=nb8`JuN=gFv*ZH3-U{Jn?XE zc(Q6@I(=NB-Bq?Yf~Nw)8v~+KK(dVxxJ}#TN5Jw-3@6r9cn}q*qCCf}IkzJ3;6$Xe z5B+$K3IZcWz!eVDt*&ruoWj1V&J=_hKlD$Xj5u%&{!=FcSLYpYvC2X7VoNyk#te&s zgFPO>=5(HU1tG&c^7Q=Zwzmt+dPYHFL52#V<%e}_9NyFT2}YK_9>Tv3W|T)-&%lj+ zt{V%@Io8}T4R?C`XUrY1d6yh1Kf@&yy~BCq%#^E6(9LTZu@bl&DgOit*eSa1 zpFqJ*%bP)i(0J#H8zw?aCcO)X=a($MP5*9z-^Y!~C=+PWVOZ~+0&dYWXi=Vumrtha zft&twDDKAXE&m(}H=Mz427Us?E(Z?H7-u*;Z{t`lxi(e7N25Q2f|npu%hP+y4-qTY zTr2YKs~JNPbg%^Gb^=7oDUSc2MyVTQbTC{76|XoegC5;lR?(dN{byz?jjXc*xuSgY zlg28>X%XLcD|0GBDVYwv0@uos{ijjTZTG(Zr%?_T&NJg_8RomzZi&oGg+gi;7x`N^nYLZMBvvvsBb8S0&5aP|tZPJ|#1tEOOZFTZ8)@oC@M|nP zhw<#T_9QYd8Q*dfi*Z{ihth}F*x@12eMeYzgq+aAn^)hPsy&)FY5QMTD2~orKGw`I z;ykYT)K~lo4NsUIxOA4ngWA33Z>9-90&{5OkBw?3F3u z)tnLgFPTq-O4;DiKJ-Cx1hJV|{y6uMcXN*9tKe*A(2x21oeA`^ZJFS*fe{WLo+Gh5>7#w9F73DB8{hw07hXkvC>5LAc`oX2< zDcg}pZCfDPEWOVQio^$o`xhono0xk9e!y#}-u_m73qR!36u#Y@;l2F|D*7-=77t+* z@slAF_;GjNrL?h zls*|R|7DBwCEdwvEExreit4_U3~jfY#HRl<1KRl zm%~Pi$3YP*1P-*b>;DU?L}XuQwnO3rr0D3S=nSTR9QXR7-PRU^JegiCuj*R-yCbbTz4boNBk|PeMTC;t7`383FG~>e#-(-bsI!&Oxpi}Qi06& zq9B+UN2yqtUvR8NkU{LOk=u^(+q;QE9zoP-wS1Esv~jP`os4~dRh9rhmU~wXemg`! zLRX^gF#$ha;aW5BR~De!haoqv1sTTfa`^6-O#2!)hS-mUbUcEqAOi_Pdl{QSzLNL- zResBk;J@^pMuVVqB#;x{?6hvIJuc?c>myo#J&q;p%<~YgUk?&{;Z3i5tfiuq4h~9| zHiac0_?@}`MeBaYG`{0`DBTSHB;dg0v9A;)i`yQ(4?l>2*?{`oLS7pL#ogO&co<&t z@$k>3u*{c`8}4{Q&>i8-4OQU)hBNomgny0qg-J<0q#==;xEMRbu-Apr_PAK51ITY@ z&>`(YkCWqA-|#of5B?zHKwuOP&h0TMJ&J+*tk=r6C?$?f@^YCW$bgQCui9@Rc*&G5 zS#Y7F==u-VP6MblbIn`;)HDlsO9u>Hlt^O{7Qon$7E z2d5>nn3TNd2Hz}3a61?WvO8Q^ zLN}))`}_R*2nRi^=IpPc_Bx55B@=7EPR}%{-r*aHm{Em<9;nJ8iHzK5AxOv4JahcF zGP_1d+JjrObxXaU(=J9Ip4PT1wK4M4czki)qp~rrqr&&3Q6~teJXc+MwBSU$X@#8- z$$BZ5i=Ho5K!_;;ZvhpUA>mrqsc1GlM7_N!#xlQ+tIN=oiYJC&-KfetOjHG`r4I>`XOJw;)i@p+x zWST3;f-=v{s_OUrvR* zY>)(!!D#lhlz16C*0a$LqE8+MuW}OcWKNq!vx|=L0uVbNX`RdhndKOMrZhW4Xfy|; zDGTtMk%3hXTqup|oAFz|_nBP0>=t(TTQbR~TE+buWXH9;utVkukY!Upe4Z-z$x51V zw*Q>NtDIY_BAJCrb{^2#k`j-y#S9bAysFw2!Qi}(k1LZ?X|Qa?r=(+( zEBVpNe4Q7E`0?^(_4Rx2z%%}roRm+j%LyZ5H8cMCKC2k26SL#@R$gDPih-ba?LKu4 zly+J7LNMQY(k2|s80f(7vmhtng(3Mun#ErdF8(a@9`pFPq1=B-SmR@9pwi=eQ=7{0 zx6@bu}#B zI_@hDuHz6pK~$+OhtBj0v`Ke4_TGEZDbN<-rg?W34_&+H^yA=CQEcb099N>T`-4`sb}WgJ;t7;p@rVe^(iG5svc`_0m({?^rK9s#Aw%|9s)bUCMY$WJ?$1xC;R@rV7xft70tt zrQNx%i)WcoQbf!eY z=;O~GkCWR$lq>n?MrGZ$A-u%nmnrU+%qs3pUag9~Em=rS-%m~H3#p3`UYjp!+UU%) zbCL3)^xfWE^t_EyQBdR+0ZLXfzU%Jz)*Mgk9kWX7kCer!kfvDs!6!NU;%u@${Y@~z z_qp|JTuQ7+lK}11y!@jq&bLzFjLG)}2?4DNax(}RW>(efSQ68JU(vtm=^cT@*)Ju? zi;U+G+&Jp1ueZJ;2G#_>1V6CkV*i)WXk{8Ge@ z*>WbEY~p~^N$FDbmuv-~1{s+4c`V~wx8w$LadJw5DKFwi2ZNCM0s6E0Tkfqmf%^eB z{SEyj>02=5$IN}2jIXn1jHO+by|SE=E|NTk%2_{ab_MUxkEKP%w=`%x`Xz1@E`DPB z-cWaYc7;X4^JxTK_CiOd(ltR<$lVsw^=tPW#;b!S3`J%gd1j)2wXi&kE23wz>wY@r z_dX6!&v3yi3mSS;N>bQkcl67c%fRX|kN4A?A06=!HslmR?$(?b`}cYh;Vg^YRU}so zPr-HOOEx^uLtpuivu{Ua@o0Jfpj*BXkZ<^!AaC3Odmd)($D);q5tXtG$7t(-;pB)*g$YR~wxThZ z{0zG=eZG!YUg8-C!U;J#yr=U+Hf@_Ke~CYM0=haAP+~zvVIieuLJ97LuAO#EMFMjr zOWF1N9%6Xup^nwO)*sRy?<~Y0^z!kLetMZ#BgI>= z)jSUwkM0%X>%NUjS3Zxat$4@lDL|p#8M)r|P&3 zFV?!m#zvMN1U7c^66crA*MGMDg8hL_Wc!BeNl5x_e=;od^6L=pPTjoQab+tp;4`6X zOlr1AgYIXx(^u5%5tU5-ol%X_M1Sfs?DMB>gEW^Xj$iIkz{qY{YW>g9BQyC9 zA4#&8=blF8IWM*uH4jW~DpXfbH$4u+s7Am}8`8rl7N;tCFf0PI$RouqmV-#A#)Fsd z^eekf?f5TOAXZ+POfCfh>=p~+!;+nf+X2t)onfa`wdpsB9C@E``X~8K5Ht6}$cI=w z-PZ7jMzhCPD}G!nvwJPT@rsIj%GN)}a}V=nG~R~8et2xNK(Si`8>^8^j~`z7Du>me19gRwVM#JC%`T`uOh)OwB71<`xEUktV9dqQeE z{{ss34Mi$lVvPX0Q<4O%<^+u-n^*RBoFmw6%3(%46iZ|Om}5?{FR>N*f+ml=T13VB zRXdDB$h44*HstIlfH|rtwZ-ri>-hqZ+S$tZ2NTb~9~-t}8q3mWvUxXZ;`m`=QUUI| z){8=KG>!HLGMx?<`N$a4s89?dPj3mWgQGAf1f)^rMv+JQCmQ;H{Ba6j^RsbZ8@wal z`rjP4tsDr_4Y>3pcjfpcT@=0}h*_M=-bNoeNXu%3KYf;DSV(6?SLJTKmR5QA`us-; zjJ(XM*7&HIj_B*U1Y6W~=Ea+Q{m;zf+h&moL`${N6v%m=z2Y4!38_qEv--uIc&VIM zT9q-)o^9*turk4W^}P9Z5#L{}dyMl2mbAqg4?_J7WfeoTyKmw#Ac$&-Won5P^BDVc zUc8W%Wk}UYL$6;yYyU9UzK;09y8n#abJg^hl^@enV>QYa`1<(hKD;{5%wiJ>ok9}4 z$ea}c8^s1|xBF6_e(;q{b#(@Nlxhj!_-Vryo{4Rg=E+C|R~rzgc#LDDpCN;KvfIbq-ERQRIJp^=KQxVo(i^*kd3(_~w*iU}H=t6f@RY*udJ05X| z`wf4(q{fr>CUMR8FTzuWW{yXTAnhQi9TH^JSxL|<)~3ff;OZjwotHDdl?|AA5}1zh z;xEWP!X&*&!DHNufWe~Y9B*&u6!&>pmWM#%nh?27Po3QHbT`pMeIb%VhC2JL{f{dj zDJI%V27iLc9E{Mtn1KqfXVnpz%GFy+DWaLzP9BO!!B5-Z@eb|cOixt7(7}w#>5YL_ zaK4wtR}Wj>z8Z4=wjNNjQ^vAO?7lgrc$z9VEc_&+dH>FPqrJUYFFb))l^(+>krIXY z{-Ype@1+H!nIqpdm3}7#gc0@6-^ zMS16=ic_JhSQ!xfDzKv$PJo{m7iC7RXfyBuiHVd#u=ENTI+qMPkDKLYuf#J_{P&z) z!=@aTLRPe$LQ+`fj@)hVCs&g#qla>)_uM6en*t{tkq-izdyZi+i8%x}noO@8tWDAV z-|Y+k!2q@$a2jn>5)=kgzAmKVeRwIv`Bxa5A$_k7ClrT_@e5zc46osO;FW2W|APK3 z3A_TZs`^1u08A9kPG1mUG5Vfr2zkA+wf=ra63 zyusmaUkLEw0O5n{9XAkN0INL_i{&)A4xB#Jv3mj9EZo(!k`-@kGXH<9wA0sNVv@Js2)pUc1^ ziGIEStX2T_Ooruv0-3-FPpmsi%G(?tpS2Q(8Hvx6it=%XId5|aQlV7$guvpcmDjO| z;qamvMrKx!0!>$w%{9RYk7RlUY6L?iD1#WNw1mIy{wQZ9hMV{(HyC72pwqz7Q*9Ck zvZBf}6QnSxskU$k6?#p?nUR1J0Z^huxGpUv0iZ-cS`6ACMKUbk4Hq92?@<%K6Za?= ziHyP^-&b=mlIah!p7g6e<{Nop+MeL2&ye#C5KM-`MS?CVg5W2~P(dni6$|6%4jCLu zdPNr{DE%at=V??j4E2no9m|s8_tfIqlcV-0Chm{bxR~2o04@wl4Qpvc0IOk8vBdxm z2Yb78laH6cW|Jm-Kh4kx`4CGm*a53=rv=47Jyd;cW&60){nj^fBIAD$U@7ZA2tf6v zh1MU=__wHr@PCRC8qM(KeR`1y9mR8WfuMo6GcDsYkKz(Sjr`K^@T9E&ECAjS`=15y zdI@k55iL?_dL0?Uw$Gyco<(6`uP$@6QQ?6d(7fNzh__>pX@FtayU6bxqd(#p3#mnO z)?y*Yeh5ZU@S~Z?Fk*aU@+0rnnDmqT&lcW5c|}rmXi?bqKlXhiT9)nWL85Yh|%=_<^K`! z-2d|bKq^uc?14$!c2BGQR2W4?E{-C1c{uur$V_w*(;;!={!^(pnaA!dW5xf<91`M% z{*^h1?{+Tj%5$+0Ktcqj{96Pn z0+S#DOeZ+#&Pd9qH-!tIa^yM-y{U-DSP)@_oyiX(nfy8=;br(({;&3zuY#e134jb1 zamnr!GZx_=4DgG5sDW#H z{6hhI3Fn;2inN$K9Xh{;fmAC)%90_?c#z<-u;|rQN&fkaser@18wle~+h6Bq2|WR4J;JwaQek|4tK6EB1L0T9 zeGb$Fs)qSP4eax$j7i?()yN0z?RfYdlaGP2@2wdt*S`}VU{(G#L116`M!}WQ#n2Q~ z3-CHV=Uw7>wZ6zbWlQAw?b;NzkE!D%mIqZE#?V3&0D^^b>BGmfvA4mwOef?tTyJdUw4=g)$yPjd9vEQ@e z=^E*~F@zfX4+0#F*8PJ3Kn|i3Y=CKNSfFAGucdxM z5}W26;qR`i`;OuKI}{Epn~=4yswbm*UVJS5Nzl zP9zut2lXH=K_YlyZM)@}03~dq=KzH|JSBvc8gL^ z7dr@b)%%;ZS+y(yG)ny@H2GENPoD_wDJS?AQAzJX8blT#a`3=qbbqe;H*3eQcQl5t zPD17T;iDkvgN<(jf4+?Z)P;43iU>F@7dBHo1pfyIK0*rMc#+{chi_@#U(+?fYGk%< z3{=Gr>ew^Qww6bbMnxPkszEH7qDU5#R+d z_(8vi0V+fgQV})yXJ$`uwi#fMnPrUko1JKwJp41AH#z>9Y3xByzio1>zuy;n1%L~n zJ7)tFVMezFXEWqcK{&c*k@-)L*#!J%6Q+rMYCEEGMlu|r3kML} zQ{T*{_cZ3uYA&8rKy@av*en8Ru00Ok&?VW-b(E(e&; zUlv+zCbLKpm>n?CbAGe;D;6I(p)?xj!}4dfgrL2*0jazdMAHCW6Zj(b>s8G~E2sGk z%~hpMdMkA{1bH>r3DsD!{9Nd}vYcw~M!W&K#pr3`- z8#mV;2z?LTT#m!p6y7EsTpqt<>$m;y9H1mzGLhzvN zQi)m6xaKxYc)fXZLqZcE^0Tg(gTMD~!dVe3LI_ntE6qGmd$;(`EN?imT?3$S;U8nY zs}KIt*J=JMh(JBxJYTuVLEV9f?r@x7c2ssCx4nTrG=!$6f&jQB4kl>{S48jXvHTDd z{&D05#5Me2p8f$?0B0?JDmRTFQ()>^tD4XEY@gFhxS$-cyEx}vgT7TI2Y}{B*p_Jz z)$KFF996_Qx)d<~+r7IMzqtMGh^H{VB>f6?W^g(Ab!ltud+1INGppy;uD38pM>vop zvK)AFkSq+BRbUIH{>t$Ejd$K{xeHLGSYv(v5d+$_S~8P=A{wa78k^tSK6_J_mOC!4yYBWOS4=E>i9Euh*N zsQyg9!UAx>F7vo1c4|k_JwV-PN6+fUA+MyxAINp;KjJ;_Dn8tQ>H>;YKS?~EM3qdF z)*={oT)rutrRd_yOaDTD`XWPk=pxT--|{Iv2g38AMW1H zio22^6d3o@1uDWyPZ@3BnIC+& zm2n#I4}Jps*9p#>d+EWX8%}mcwSdK)#zpxqMN2TQsrT zxb$+MS^nKX*9;cd+=bl2Fz%*bWT6@58 zeXGC>#^70gkQB0|{qQoM0Z3~lvkTxqme~31q2HJRx@)~9k%7yts;>q)(5cOST?Z$m)0a} z9(TB6muYkB=Mt<-8}D##Kn$J|0*^t+O% zEFE97q>YpOIZNs1R%yeLNk39MY(|@#W8#EnKU}_b{-*m^#cRPcU#l)-XYITy_4fuZ zk51@|`&ZN`iP{LC&VXon!o@U%h2^ik(7yo7=yX<6|K4<`s7@z0P9V6`R#B4vFWV)n>?yc$HZ3$l0FYbTbNS?Pa!*sRT4(`|0iCkKM|KdsY+QyvJ3|VjF#6Suw!PDXcBTrU zcW0%WjE@ciU$A#>K3?*wj3{mP&%O!y^59pekWvjY{9rbEc`SH7a#`mfrTzBHpHUnS zis&M}Rte~`!oIS9H4QH|4njn|4w-C_qS(f-Cv{r^rSD~Gt=o6P{-&U9{w6Q}9DdGt ze6+0f@c6O#Jyp?^gR)zK0k&pF!0(AG6R96`TYK+V-`9;IUJ^aLxh-mQFfFP+a-z*L z_vE2iz4PC*+^oym7U&v&Lmu%|pi>>r^|B5JEqzZ2UL^U7F|x_MgTh7}yzt`j&k>Ie z;YJgE4}Z0Q5IQ3i$(U`xEwO#6@N0uhUIiTq36%>GQEl>qa9vqxlHiNfPubOZa0Os3e4L4sB1H+xJkyaG9J4-BT}@Yrzg~WhSg|XSn#*uQ`1H7 zrVA6!=E#nxxlQpA6cE?Nc({c!Ygz&7G{P_Cy}Ik=^s`-JlgJi@8ACJiOo!9<2x^B# z!n?F0*rLe&c(@Y=^{7f*;z=_#Qpm<75js2Xj8!&)=qcS9cp)M0Nsq8N$@r1_d12`c z6r|wKc_mk&mTzsh^-QJJl{_n=gj(sUq59n``9-GM8mpi1SF1d7kB%7+dCux`3o>T+ z-+g_Kv7|TXZzcVtDOjTt+ZS_bFn^B%&ReHgJgR8rn$(qGwByY+?D;`NhAm%w(hiQ( zJXgrP_>$g%!7`Ie%s{Qv-Au9Lbma%J?DCm6=By#qh>HPWwyV-oX2}snVpaJDfw!-i z;qfr}_E%T_6vYp`xstzmt`xSiYEk}78!D3i#>A}TN{`3TlgqOgtZtSfmQ$~kAEuSs zzLd4D``&IJ7x>0i!_(Oe&A@&acP8w%r;DH){{-g6>%{a|7K^o4D)@F!o|N;%I`=S) zSmO5IKI~bSKIO{Zk`V``4Q~3U7>Hep4N|e zCr)?SPxW%QS(9|Q-5ur?yudB2Mi8E}$Jc2`EK^o##e{2~u29{loc1Sp#i&(&7pbo! zZC7g)ALog(oiB#B+b6X%4vKKGof>VIWLC0BskbrJDQNU;R7|VG!e#75d;KaPNn&DH zg=JpRGB&@gu5~w#=bzfkG25pYS?BpAFE*chrIfoku_6 zJVc8Oiabs3=f(+d3$FF9oeB3A}$cpHSql#l|e%Z79Vf#JNyRURoJIb`8qx*w#^wph) zt#X3A9&NXkK#TUz_-@I}AY8TY)wXzTy9ujz*DDt)EXi-hj;qbFX5QA5QV1 zI@hkY=^ybxeIZ;zg2TLGBh93KL{{<;;m5`L_)9&Bi#uM?FxMxC<#|_YT}|Z4a@Rc6AhunivFs?U#6^vj_0Njg$%C1q2fLoaHL^S>2G*F_J%^8>Qw0_e z?o*tF?vmZ~I#cN7an8AR?UZ5mfrRFxhMXhK0bTWLHX8nOpvBaNkq278Q?)L}VbqCY zafZr&N>AUK*W4Z_Z=cd&S;y3{O9qY9F5S^QctA!zA{!8)Cnj+qWDK~5e3cK6BYlVx z0Wous2fj9ecN^W*KCdLyjC84uXDuD%UVFB`o zuZTzQ@N9C;E0JVzhh_sFkW&4-I82NHl3x0*_z9{V9Hv$$JAv8h(%xB96$ur07{TwW zX)6k}oIaoEWbN;#-8C+OGB%REuFjGHAQypiA%K2*rGzBFh6(A5n}W;r)2^4@bGPBI7nA zDK3xnw%HrhDdM4IJ@H(9yRuer4TQ!Oa8YsB`{B;(f{ruqa;C~j zO6|US7Rlta3@$pf+u$0m6i(GU%VBsDc;n0M3A`-1kjpfVmY?6SGvxj+Kc9k+v>|_q zGi{w_|B|hPc<9iKrx-q#GnD%-L`S}qLW|AQ`d>nk5FP|u*2@PDM%&jrGQ=C2OWN-W z;9tubMs#=NE`Xl02lI4=nRmQ(IMR^F;Y}OKv7h5&%E#7|NqqSynWtpd$D8Jc3j_`=jZHBc+-<^f+So_ zrXuiJ!~F@!zX-hC&=9@J@FK}tO6nr9w^+e+VyCPpA+9L_EM99sN&Y9KFq44%8&cq; zu_55+Z;e(AUn1~|fyEQ-znV5G3@@LUk)h`8|CSR}%oF}{?y=bzm>qT$jj*~r>E<<)+aBFxja}yXn@u=SAC(oyS#YLU1Gppq@o%?AhfexFY>4q5 z4`&P;^=w#BprcH`$8Knut&-@6HD`CU*lz2u+%c&N)%pAgrVflW z(wj{zjxylmn2rJIV*t4?m2DX-Z#jYw#mQ?~RFuFt0xZ9WUgpfO8%akE<2gF z__H&@@cXhH%!*NNWR*3J^UNR>WZ$g2L=nSVi8~s7S2td$w0iMX28;uicC7YJtORGk znloT%WTFZy4YiXHeW*|EB+Mxo+ZEucyZS2l@$Yq$I>YDfwSeVC3x?lT04E$c4nAn*xhfQh1uDLoU zUT)B&wh^*(I{&s2j?KYZNp`pygXn34u&JT+D|zWKl`KU!(iM$e3(V%YChV%KKA=vD z_u!l^TLitB>X4p~E$5}-t;74^uiYxWCRRVA9A;6eZmsc#+Pko!-Z(R90h$^%p=LJf zM4UeNKV1Z}5orofp(5%zl{W6gxtr~C=&&nUM>Y%7*B(*3e7h5jN7F7EHi>37%mBjC z^t1pm7HicaNdyUC2jXjOZkEFCjM&(=&x(0ioZXx)xS9Be)#f6@Y_9$s-UH2yqRztu zseCk2Cbj|m;Xg-X)hd`Vr)&Mck1A24+(;7SbJ0;#9TT^w3-~jiy9;F_bAAkV2nG{7j7|DF4578acyy)NV<9iYt~Crm zcmwqr$$!yvl`As;py!v)(D?MiW;GU{_1gHd+1YzoxC<_fuf;QNj>|DvC6vC)>xEE8 zGVDhAmV#GWO)xIca1p1^}(I8M|YaX4&4&nK|6YJNAWH+HG4e*#k?zCj?=W z%e{*$49lxw0JZ%`SNk^m-v+||xz&Fg2*-;_a&S$|l09MB&f76F?CMU&%zX6JkE^O~ zk#HBj*#wcBvoa2V40H$2|IHwD_(}egK}h&)iu7rRvHo!e7%`uu?R_=g z!AtQ_{#G;@IQV@v1U7^8oTo8&9(8oSg^h<}ug`q8gb#f=uDwCccu%6D6_jVV&H^DFf17Yt4+d6Ry))t zsaoSAdQ(1_tx8ode`FwcAGz)ZO!*O)q}TDH;?Brr7iM5t@d;Tld>uZpVQLNtAJP7I z{owZq0pR?XD+j&nPU)I~p$%N5{;WvD$?OJ;dWkchau>Nu;J0ua3ZoABj&6eCkXz&K z?lMqDByL-T=6_+zUHkthQy%M5ROMM4?|}0NW_Qr;s|k-1QsN9g(FFDKS8Mg2qpl*1vsVs1VI#n2N}zv0u>i`DIsY) z%=_BU`#ur%${1bC6L-!<6PS24em%JD^A@i-7@W8??;Kvrgn}u^g?zwhpdyw%8 z1ABj#_Z-N9=-J(1_~+4Y+jjeq6DLC-@r18RdsF6nH~YM16R#u5IJ*1S8PRY^6D-@Q zDyjkRxczx&H1OSQ;NsRt`{d1#>djFlcRMup?l6oo%MSUy)(XnI_-mB^0$a*i|DIj|9 zKwu6_;f;jTS%zFB!#uJMEPU`8Gccchn40q;vsW<7+}`fi-r1*zpW)X5bGSjEuKPAj zz+f*g@H*|C4_k@X=`}-sntxHS2~m87^T@*a48UOdT-QrrZ_D7}oMCF;@s4s~8t*^< z5j}Xb6{1;l2)-V~#z;nshwA&oXwWN2j*#z9u!i}eKl(r}w-ZfX0d5qS`)9n9coKkt zdpF_jl3}43Se#C>w>*5J0&mxZcQB0iK>`qD1Xby|?3HBIAEIFa2}=sL`Tivh9?JgS ze>T8Vrb7cs3NrN!o%t5_z7H#aICK&Frl=fFr}fR90#2aB!7PuV-p9d)xZvbtN^llH zLyk{<9}kd@f%!-*;(PFSScq)cRhx*rD__JB`@r{)Y|b~9`(Rv8A{xhGblz@4)`9X7 zq383zy7b4!iXFRA@J_%_C#_@r^~j{8$dsDnw7JO4_mM%e5mp@`>eqwsdVRO<*kw&R z8u_+jZ2n0e>tN*jypSUC<7i-4eRTopKZ^#1cjiyw71Hzc3p=fh7rNcgJdAj+0fNCQ(i zqhR=|*s(MD)&ol11F@guNs*Ofzfzo|USE%b%pYULBGi~}JQO$xQ~q%;KlsUP0PDA? z#Rz7w9Kg7JBtQ1!jn}Uc*~n3wZyPp$nj<1pZvRPnbvzaE697&~2&6L=|Kct1v?-$M zbNg!^pB~gZph7NJ)_y9NaTut@?ra~N9>}7ONlW=0o*Bv!50dsCC!mH4P_){$J}*QO zNZ#7gB0yUCLC*v>o-dJ+8t%RoNz10b#o*+;#j z9CmRK2KlSo#MPc8HtDTNwvmFNwy5uiyPsa226@`Lvb??#QZrhn5kx_X_)dc-E8C+K z9WfU`S07qGbsZVXWkNrNciolF`1bN?ydSfC!n<*o#Z(T(KlK5>l=gC79uFt@9KTxY zP3J-={-k?8S!ph_UFP2=oK;d_I*YFUde1TW!8D%-Rmj_mW^w?i9VO*F4;n8pUQ8`M?M| zKl$cnVsHPc=&!I%PWLRKDgh4*NA}#?;{myBVdd9sxZ~cQ^&Xdb+VJgdWCI~yC`+5x zXIQ4=r22|$CqB8`@(y2Ec}%Y2dxx|V$0J(+Y%RtfUR_$vu7i|*NjZIoX(^5#r1PlR z6JK}i3c4jQYIFads6_5c%*eCjl53@5ic6uGARotk=jujlos>8)I@wn}cSVb{jZ=9f zTct2lNf%R8TuZ~Nn|RZTnhSEhVrjXxV%_+>#mnmy2ht3(%vB}pqnQD-3q}%Lu0h`l z#TRNM_I^0uk8!<#LA!Bb*nMFlLxQ?5XH=ydZmzPB&L1!S0`_zMCDUr3kLj&}1duepH<>EVC&tDqQ+)Rm`?*IOV5vD4|Mno#1h6&U@t;Mbw?eTh1- zf4rKedGW_)F||he;c>^sU%Rc8izf$fG&O%8y}PaX=f|hp;25p{|5$jKkrw9hBWMXh=Dw`c0*K^>OlopvxrUgqoZaYk1Hfal_kwxTKOed+{vEflB-^pFJT*uce z_h$A!%E4IS;mK4>9b6Ll=_mW%d)T!olj4}Ie5y!7*6p4MM_U}*3GrSnpa zNexRHz3ubq_-H2GeTLtn-!7=6{u;=>Qr=V2Ttq3w69z+y>at@+&YYRdfNY*wUy7%j zSffMmV4e-o5z?zyqclQ~oo;oEcnQSx?Nayd0k}$fNRf9rN#Hj9G2)z*!$G>tz)SFI zJhaJG3&Mx(N)gI05ss%<-zN&)-0Kx{PVUM^uce5Cj~$SbzH7-~*{xz^~s8%g&eji^RYMfofikmjy z??h_W$mKh96~vZvsjDCKzzEOaQtA9!5X38F^*Qrb*m1M7Y5vcR1{AM4h7F2#f7q$z z`NM-verT5#&k>U3w>OpSI9H_*yEUoc;;XMz+(aRN7Zpsqr8p~k zondzf(Bx8z5j~73Z=6Brq}#V1o%>P&ve zk-yFsE0$gg9$76DwC4HpDDz%@Iqq4tkn)7l6rcipa| zx>oYfPd4}3q+W+q(+8B*#4L$*p4VJn_`E1z7e2P(%71QgK=OJ5^94?qZjdOHpzu{> z0mg|Y$N7^;QS*L|d~QD$ucvZ1T^ZL2dU^@W*D~t+ z+|TWp@%>YpxBxx&9t_;Mw^Q^CY1HnUYr;m76}Z$y}8 zf69}_V%`{?`sIN~GKrPZ5?N39BGlLvr?L3K{E}$gDFKm+Gh#ktik>XDR!u~cYCU?q z9*tdlC!fRLi68mlvv&vgbxXOM<}%i*9KsPc049>+II%hKmXqyE{VJ0@R?qLgh4Aj& zLSD=fd2tJYEoAl*9cLJN6hC{*6=~FDDbaGbMnhVc_A~~{f+pYA7D37vS<-U%kZO6C zL}b^Ex-qopABm2fL4Ci_^WFU#W<;IQrlPr1H|i zqEDa`FjnKssjbULwR=*Dmu~y?b%2$KA|?-q-mV*oQ;Z&B7KE$P#;whc56%^b=7E?oBmt#y{c&y_a|+3P}78^WwSs3aattV_bE zO6K^31{||{_mX}_%?e8efjOsfFi}fwHaHO`{u0K79|v>p z$KuWp=%SXAI~mYgE{eBQBF{V83tVm+8B)!m^!jSZq}QO<<);Nf+YWKGXj0st=YlO1 z24Ywm$V%`I>&M%CXt^k$%hLk^J=z%jLNUSL_gE}FzhFkKms`ixtu~1;==X|$A15YB5U8d z38>wDnq66`(fYpKJi41+aZ*Ppyff($?yYd=EFQ{^gFw->%D%O^yk*JoZ`Dnyc^=P# z9NYYyrV0g&kK=h9YVj7eg!z8B_WfLG$0^323yCsr8qR4(9!p^mS2Rh+?s(x2iXd5s z1o*cxQYHhc-h0cUZ8r2?QDY0s`V_H1o^iRQ0Uudp^R&dR@9e7d&|;# z^-00U0ezXB4<%vUV=+yay;NL z=ELGZCM4I5IU5I2j+@WFM>mMYs0l);_JQgeA1@ZU-48=G#hkYZAFOuE%eN``Zm5s| z;u8$wyv%5cMT(%nN`j2qWSRgu(1sRhLk<)`rqLE;R6>D8sL!=xCO@D<$KYB_fQ^|afZv=oG2Ci9mEay8hmbkxKRV+H2_pB%G`m2`DQ_bNPtEbm|@)E z!}Q;IetHzbhybT9^fSlOZ|)=Wvp_wtl$OTmbDjw=B2o&QQcP;0Mpyt&5aq_fDk71b zg0b6kH$8Ew@-?XrkvBNWfFe6{AQ?xYeW-WABXFCP)e@?aF1kTiY3Eaz=%CX4|FMdkF9Q<9G!Uj5^xGwT@ExFDM_B z#R8I}6+%(xeIQBOsJbjb3I#5pC?-H_!;3w+ixo0TT> z3TG_$!_+rOr5l$)YbR>sC)C#@PY&#LW)UbhOu0} zF*&TfpY?Sz%)~akA~HL(^X67TYR-4(sxa85mMQ~vwx1C8pV@vAlRg#L??JVULrdgv zezqwuYqHk*vx1HCJLU4d$Dxqxbj|sH8GR!n zoSS%xn+P`|(rk{S;BvHF*kFA$&hYr27FUrmihh!d-Y^SfGsyai2ooii8aEf!ap5_* zifbtx(Pb8B`{eK zsYWiOry-P|WrV0!7~-I7f(TwBD~M1kDo0Bo1+@R*{NH@43g&Y1Yt5|m;{>bv7&!cMf zx245zi?F__5;)i>>3>`IAG?RDAE(mVFx!?a9j+oLiBil^EtDI+l?tV|L|$5@v$tg0 zT3MU(01sg%D~Ui>qdniiPWg`ROAu{=~2Lg9MS zRP=bu_X(@Xvo%7E9th+p_ww^wSnkJXLT^g@f0mL5U}zFOH4s7F`yWH`{Du83OK7s` z@__v0qFU#o7i+{9r{29_wTI4ev%W&nQ1^qZ#~XFu!S;+F`WC%>I9Oep3UViA(EbDd zD@*({{7(jGFC$^t+VVl7bJ640p%)(SUTE$^3ilZd7EVENOx%{RK)P2)g0J}0>cZa; zN>X9>3;r4a^)09W8UO^83X#?yRmAY7_D(T0+vJ&M7WAH4^9@p@)+)WdCG6UMBbMit z!MkS%5>GGsf_>8dssLtoXa7|J0;#tEssxu`#4NwYb_&nyLQBBu!zm~M*5e23wQsxr zsWF(Y<)G;0qlN0TmesU>g8)sR{s{umtRh0k!65F&M!IJ#evK_}T31b)H3^_tlT({= zZ9Qt8AKtwVQhU`U(Q#IemO<+gf*J?FbZ~e7jRR02{cBlFYafXl#*N6uR$BGuLapY) zeS}@>sX72&Y|?pZ@pb>3XZukPMR5S9{a-D>8PK3=0ix+BxF!7++~ds_DjZ0XFQGw~ z&=_GM8ZIF2{a$s4&W|@c@Beyo&{_%sq-Y+1rL%f~uP|wH2a-B9KAl<9+OF1p ztE3k_3}wc4tHjdt?Dr;`wyBl8cBQ1Z4e~s_CO816ngN1cKo7_R+m2YJWgsJ4Qd`yO zUf*o%j~RwmruQn4A1fTtC*B%JDH&k(?+iA1*l9fYR}LU_q-+2>`z=Ti++Xn7eTQCo z)9%}*LzSjbQ%m@m{i!Da%znRj?AAc?&QRJ(^QB*)Xde2hG;;`wzR3aAK%{X?YrWy$ z8{;tCHw@Jyjyj~DYRAII+F*xmL&YW|Yz}RD_Dm4V*HlFhJ+o)+4WEXu3N*+GEH8IgPbPecJ_iqbSV{FG(P^AHa2#VShKwh4J5n7|v zyXi`4Oo6GswowI&*;Jt=EmIqdSLy^uJ&3D*f-ht28sJZ(4erjG8rKE7fftBQ+WV+( z`o;e3lW|3}DK<-xo+U7?HGvcy+aI2qIGN%J7_$~+Lizl42(H;rf)A+P5b?JZV(sRr zeDtuj8Puk9&Sr_E*hv=(jNuq&FaDmw>Xv}E`u-PP}G_FPPrk5 zjD=5a3vA;H-%0S56P5u0UaYxJHCBLC;807iOjEA_zfoS;P5~zk@RzqXVIUPYm^$P^+r_BEP1-I>}krs#xcKB-a z+-rtqvLk$KiA7kDi8gb&zjU!WdZLzL<<7Xy-`oIpg8&UbsNE@lTwIQ+A54{8vt*?LLfguvhKyT)#weQ*IWL? zS?M79je`x#i|=Jd$xd73UFk6tp^RD~aLW8!AwV$G&5)q4e=m_J0c+o}Ya(SEhB1uV zmY}NrTpKOK9qb1y^Y>xPp!Wfr)~{iANMoNKj@*txEtRZqVxS!v+vI??jF=5WerOgy zR13vKiyIQ7F4zA-=8b*|{_{zeas6ZIB10BR1_iFlLNM?v8ZtKaDEx#B>Ev--5 zm|Y$6mISqKL0u~%eQMqNM3UK*V_fIcLRh{VD+J011OK`RP&1e_26~2ZadL0_x%nq8 z;^z}as0A=C@qi9P-i1<-knvAze>M)a_TEP?GZMAo|bJqciNqa z+5P$F;CC6~z=;KgIW(s1F7xk^8FvIo8xUrQpVk-TGx#|ys*wPnT0Zy{xT|aN^*19- za+TJO_|^OR{>tl5dCT9DHBkEd#n#vVu@YREOtBkUTDv*|-&~oXm#|Rh4y54{a~l>0 zy>q<$JaSuBkJe654B!GyAbGPcQ+$hbI0^ktX%ETQ-)MiKK$^u*dye!^7) z&KkxAwwkOQJgt>k=e{smRe|RBFm@oiKst+|VT^k7Uu^Z2wSz~_*iCO=uH*=T1)a~< zdL=04XkImR_x_7Y-V5_DQ|^7reWHf=`Dj~M&-$g$%#$jXE|@-}@uSgz51YvxvfqoI z*HgE-IiJLKZ1Ea$YMZ9g8yt8n&Y$bOEF`u(q!saKsWswYt^Tk{NFIs<(J|70^R($Q zL0X<{p3%BQ`%L5J4s&Y~MNe7j`{))YX;3y*=IUgF{jD!SZ6bCfvoP53HbXh(F1r-b zMqf%vy|0>7>=r=#iZ3yRukYq19<+Gi;MIJQ;NyOcNOZivQtSI4M;0|Q1vdJMPV?rC zU*y3SZHWUFq&Bc(lr}@E)V%hK;k9nR*CXmpx1nlvTQOF?Q_uKS1FR~=oZn5NP5bb1PRdnODVTpy%hlPmJryk2hWpY?bo z!pJL{T7?)?tHjyz=cAdd#RF_N_dZe& z#Rpk)hC_z;({te^@szIS_naQ5=r8@KtwlgDl;0D!SGtVtH1h}foY2QN-y+_r2O{ z5{!%Pj^Rn$VZ!@|?Jp}>hX}|xffA9Bw?6OVzu9`j_UqWC8)qWj0=xAA<5ZkC*@2z! zdEAm*mtk&eRKFWXa^%WRC%h!NX4{#9v6=j0+Z}d*Jg>^3};9ufvUWJ}CQd%)7A% zNDV)6TH63&@tA^#L8WI=Xkq)EW%r7Ez14B}d3VuBucVW*?~Y0St1_RT&5aZa%_{!{ z(bhx}j)S+WVlY3EiNWbi(ig{_zK*pDX!tQosGdoh32-iqOj1hOrQY+|>>tnm+G-by zc~?gHc0%UXA`PaB9>cOaI=mgxaeQFv+*&N8%XumGg=J*|=YckjcpMDO zE*DFCW(|Z_(KvI`bMDQ!&Z*+s_MM>5DJ+$)eZrTRUqy}!@yrXoInxDhJsK7Y6o2s9 zH&TVO{2=}F)csv0*SsU%*koNPVLpaqoGI(txv|JKR!yK&(~3D2rAP7;zsoPJl#mwa zpUlA%th z2Hl|+tx9TPQ17uk^Jx*m)5$>iRXtuC!^+1koW*(CEJ%`H<-F8|vz$fDG4)ocpS;hz z(5aXP+|5YNIlTfa(^LE}une{w zOQKeT@&;E3g;(9G=4*8?yHe{P%t>Hpnl~SA#Fdy6wjt`PvF8;%OPlnx)HL2sWGZEs zHiqJ4B-Ut{8^^+>D+kVV>GV;PaW^D09+tu-G~>dbhwc3G(vh8{v#@mM(jkhgoQ=9_ zVW{wsC9KOT_b1@0>RBpS<$JB(_!=TISvX)GcJU!fftC~}oKuO@e25v$i5m|OFWxfq zn4djwXI0(MT4@kI0Z-j`K^O;owEFedruwJiL$he=_+r$2_Zr0gR3S;KEw!~MGl&w& z!0hB7Z1}>=Xd|(u!oL&y)3(@iGVpqBzH}YxY2mOdnDrdMlbAIv)TY!(#P6a?aHcrIa+t-M408qzK8W(&OiBMtb&!og&Q zmZD3!+qU~{ILu$|ah%PuKB6`3-i3Mr_st{t(T}gfVN3X%yT4qHhirO;VxEdced6SW zqI)G7u2iTlyfF`ZbWrRoI6Qfd2CG#Ukbyu4Dm?p1)sCx4sPcQfow6l7ZfttayQ zn@!Kiy9|A{h2Hmskt+RyKoBVEo&EZkyzfkT2b>Uc_X;4^)XA{7n_a;x0Ix*~^DOW0 zKR4;u7uwW&LI$Nf11OHU3+-VsJL!~@`87FrH~-i>_wMs+ukWd&zU8 z#;6UqjUby)L%v|tO&vXfpKAA`cOxz$1;%z^s>KS-P?z;XJ1unC zQ|7^ZO`SYBuX(4n=+)!2?9&{)!KX|~%u=1KdYz}GlBV2{7lO1TaGhm$NfuN0QX)aT z!`gg;P~PdT?Ke6M^zi<}bX#%gX}wO6Ue_sg9mQMlPy&)7cA;a&P4oLFj$^+j$w8JB ztwc>yr@?%eKnB!l?8;|-Dx>|g5JE$Z>6Lwt%EoKbjZDSa1zz?HsV zXB$72ojfb9>fxZVGA@|p4O8@ndM6k}<{0?4Y1<9!N$Y{xOES)VHE@MqqHfVM67Z>n zu26?|(Hx{RDo)9JAoA9LcTV5U9oQ=USC zSa5=J{Rfz-)K>a{@5_FJEbZ5VI9X1EJJf^3MI)qKUs@&C1Hr)o`&5ND@LY8&@;!LN zf56+__-0$DHah*wKE8zhQkGO->4_mS625j1nT#?XfY7AwLwK}|U-%E^xeryl4@J_) z$%<#$qQQ4Ij9PY#T6c`dPN+>iP*1vni8r-PJTRa)GU#ANhq;?_Y%=2C)Lxlv zos3hCgp8jIE1`|}_O+JDFh=n{%7BB}+Kv(9Km+vy7L#n2NKGt|MJ<}l7JEh(rG`yw zYrZ7o$-JX$4rYuvV5>NpnpN1^QKV)SQi;$HIit)4qaXE*7yVGh8>4$U!=E>BUMd%p zl4BLcEu8G*ISEJ_RML^d*tg2jkJF%#LT2Y$i(ebKU+I?4L{kY&66=A5`orw#P$WH& zpj;3OmLC5$JQ9NGhd;o7t{Y>;1IU00_FdRqPO=$}di=$rSio09jO79*-fc{v+Res? zLA09;U_)Rn-4f|>nLX?>G8(=i^_tpH;4WpRHWc>t+UWo|20g}iG$9}|d_nz=BYL9M zbn=&jm1w|3Fy|yCyJYf<++fNvL>iXJMVMl0zasnFDz`03G63ZsnJBMlzHWL&CmQZi z`;Vc4o%+Iw!)WyWR&5KIA%ChMAU!;H-Di8I79RUa7kc;+&Lbr9dI7ZNApo=lZB3>~c-v|ch2knSj) zZ>e=ugSGVUaXZ*$)984{p9;}n_-(;k522Ij%U4LE1P zG?kFi=Nhz|^e;^SxC;@YhI!XmZ{1&NYg-zthiOtJy}iR`9VcQ-2T&d7w#~f4dfS&9936TC7W-$c`s;BCekx#b#L1#Pbno5RHLBjf z>}&|A_$LQj+Wio*?W#8K!vAgckD~)60BT*?Y6~DJucep2Dmo!NUBveAvedojalE-= ztUU6*WZ*xd2ocG~87sr@F1zKjySXwSu*f&=*m4?on%_yue1+Z9MJ03nLSPay5J@c~ zyjonB^#-vKlO>VhU_pcq|At)55@VU8Z$|RKmv|?f`?}eRXrsGW*}5zm2cX`i9w&ge zH)#J*n+VV}98Oq#)9Rv!oVkZy;!=S$`2DUTcpe5T^H2_SF)4GqP__;m@bE;Nrcu?A z@%8tcICjCU)Ooxj8su*7>EY=)zvkR+O*wqcczNr#6Oe6=gQ7`^av&k} z^m9Hy=-4d{oiL9EC6{@pdU^`5C!XfSY@7xlgw5zJFY)EAz)_EE{tr-cyy}3hDl*a0 z0WF1uOIIVglYI*Fw!@V+BNDx|hzPNC*sVQp!YFm!-tVsH2P&BBFkt)FAxYkPeTLdT z040PY!&ShK61S6I`$kX~7vPDC1beY*d}g(~_^fB3%tt61_nguDMIO#H4+G+aPt{NS z)ksK~MRAA=Zo8n&r~S!?lO-2fK`*f!-2Fti+ZX+Nto>ieAm+jYXa-=|#=o(H_jMCg zjpV};lI|Ep*-Q3r`{O_U$5joT6siaJLi%N0-0AV$on_pf3*U{ePyX9Dkh3HHZ5$-c zY$C0jz-I^_lSlnlM?WeGdLOmI{PWf~_dI9A1Gf|Rw)1d66YTviOn?YlQ6Kp0kQ{fW z&EtiS1IV*}SNc9)8HMIw>+^ZCkrnf~H#2a)3`_kh$6K%`?QhEey-0>Q876w~;dsRXN^Up_ zB#z^aoJ&_pJYx}@CY~UMJv=4*8Hq8EA|5`o_Mgq%lTO;@MSPKC!U32hyMll;E%@JZ zL6+6VzvTj)OAR_xMPSkz2afR$KKQotcoYiQ7}$FU-c|aj^*&e*5u&Rc>Pftw@vl9S z)sqi_C=Nh5w)azTAz;$i8DzK~8~DH=h)ol0k1(O0JoH>VlFHxz!T(iT_A7PrUsoBh z*Mh&2N&4Fdp`Z z+%T{T*`Lm1$L3)GeBPf`Rxy8MeuGpyAfAH=62kx-y!1FBXoLw3fKmXuNRfVFHHQ z4b~Zv+y@ny#lNoNzKD$zbdp%4jdPruD~&|t0*2n7y$^uOjq9nCpFlW#KIktEJ|EHI z+1%p8>UBXp$vA;1B$HgDbZX$Tpz?!^8o@RH1o0rGaq|lmk0sy1qBQB|fg%%Wd47Y6nz&9Q zPSrZW_|eLH)^96ZdJUS2qjsKgh}Dktd78?c$!n?!y!#&>4WsvX6Ii5T9mZ*b1TJ9E4}j; z)^GU1X|Qh1>?dDr2Mr;74HK91eH)oMh5~vyPSfu7y&}h@O{Zo`%FIp->%5t@@7JLl zmS6Fc9Z{??dKmww@!@kLZ6m^6(q!yZ?BeF7wY6r-Q7{-9TWufr8$(pBTT$+kRCyDgk&@}2MTD~lC*l~ugG z^qv%-*l5#w)0;ZCjl1_AdXD-&Wtciw%45MLKVIM+VRQD6m|$A1MO#!aJL1pJzULe< zk-JMF?M-{F_oSO&--BFVt|~aRJ~`OEKV!`2*BN%HWfanByb#=WR#kL^w|N2u*fk2+=Ql8*;$ z&c0l&lZ%0#5s&ZEbviLh)+;G{+;a2^eL*_q**hlyQL92UyGsdjc5|Wo9H~@1f=XMW z5s{160c;lIxUaq~Zk((&rcMNh&A;K7YW8Z)oS4M5>h@6|+q)9RV%m99EjJNMcdqC# z=bc^Ut1P69vhQcC`O8v}EX@n3=L`R~MUNXV4~(#fA#&T6iHgGorm3C28Gk~yrUjdO zPK`b*I&7B(TKSJLhB(_SvJGR+LAI@Y=Lbg^-65Pp}fVQT&rQf<;kJ}7+Y z=3`gsvr7J%yLeIyo5vM3F7WXXQ$v&f`TIr<%6a3ZsUfbQPw1->e}>rXhadaKONu&= z=y0w2uKPc1w0t*u?ui~7kE$yO=W1l#G+RaQ7v0-nk8yM0y>eB`C+V7BqL~L(%;J4? zbLptBnoT9nUw+Mm?m$9=CvNeQb?bc8k%0gI%0#Jg1NFxT{la%?sNwNkY<(aXn)HB%Uk);QH9G+oufgUy z>0&rhZ=58F*=K!D63c(~6nEkIS_1%%lU%nFH;=742d7VJGE~yF#czg5posuzZK}qq zs_AO;8ju7EjlH5aol8LFNLLsX7MStStv8|>^)rWipI>MDq!99rUNtd@d!X5WqN_9i z<9l#ev=8xtNq*G)r`~D}`=Is4mWX1?4+iz)tm)bQtp#Q6x$4fZnkje0kJq!g;giGH`?yOVGU77Sr<6eQv#zg_k)1x)-2 zohiS6%)xLAGu@7~qVrqmKu#?4Lgc#RH^Hdh%L7irdz}&8S<3EiXW$rhh-x{Jh>j^u#wx)?(U6z&EXWZAjs$~4-Qz5OxXIa%m%3{6B zqN=x@?^{n#^@cC@7F6Xa1ye3_m3K1CEhJoIUl$kSWGQ5z54el-sV;6qbpz~?L-)>Y zlBcv0UdgH-*|{xSy{JA&f#c(qus1yMPO43#*q&^k2U)sZM9-=~r_CwEpiS(t5je;H*L{*;@Xee>J3oGljh!%G-+V9ZG4%kE3 z8OMXKn+|N7Aoxx$(n*sl%LfD+C7o(YX=0a^)`hZSe|Z$7715o!RdSpf)VIg}=|EUf z>N!$0a#~*AJ-;SKD1<*)H`NgZ>2J7_fwD8rXk^qRhaO8{)g$J8ssLE!Vm*7f@I>iY2U4^KYn z^nDIik5~%N7gU8aporfMU$cUbKgL&-Jnyi*^yBb$4CBC~Sb0VX{)P8O-cN3NRsZs9 z-D?qi)o-*S-LfrKGTwW>=C%SIBgWyGDmnD!pO)2%k2mVq!n62qu3{u5~xI(uI;Y}YtlNMJN{ zz3NbW==}X-zFXo3m0`V4L+uS=;O63E zcF3=nDhtq|AcdBRItt57>lo3((7%GUFSYMkMaF*2h}r}C>VbF}sR}%ck3JYhjFlO} zCdQs8FgVsc_cnX%A={nzsIgmx7%OI-^c?ef+*8QVBQx^w7w>jXe;Z3l7UJp#hzYX~ zy6KWF)Qo$;(eAAQev;{%5bsE;^N{d=HsA*jW(yQlZcHphxaRsXjn7xhoDRrLiF)}C zneWx5bx2}~t4k_je2BV8c7b5FUFkcv4QDfgZqhnnL_nc~@3@lBCA{UUPgqwIxIpX8 zSDDkNWYF>R+2Nfy)-zJloo43b*gQl^?|G7zxr2Ogeny+qr-AvW^U-(0Z~MCCH7XQh zJq)7VhOT8TzNaXy#S#MtTX_UbQ2Jc%=ZhCNJegE6bXdj&4!oe(g)lDNP)0(&V*17q zdx62(`h)PNV2k{iE00G@c9Ypo@$*xfFR`rs5903hAL{>q`2OrOW-Mdh8L}mWWGOV1 zeH(@BMksq@sSq0b&WP+ncHuM1zK$(h)`Vm&Teha6RLU_v-{W_jzu$2?FPzuTzhJJ+ z`#p1ey&li|(tNK1asO#gNtdjX*#HT;xcM!={eNgbgcK+k0jLj~%l|?q{C8;PiuZ zNp#1OE*vpz<*aKx<*SkgwN=I;?fhV~YY?h`jp2-;o3IlRee=`A-xW8i(MM!T{PST2hvdmn6|dcG~WA84)}X zp^OLHP(azl7$Loy1fz=T4Xi_a&%AK`^W?XCPE7b{U)52Ls3RgVE@7tLJtK2F^0^35 zdsf<$0vtyGWE6UMw#`;qGaoMJl)byws`e%4u;Kj8u5I%_0xNy+LLhwqVv-o*sK4)G ztu+(hG*3@Mes}#$TqBkC!M$#0CZqBd(BL1Pqhx*#$3M>``ui0#`oy&5K996}&5 zCOZ>y3Mf*MojUI-4$!H?BVd9SXI+xF8k5o7xyJ^)3lhvA{f9} z#Z1}7GG)Dm%9NY1hM3B|Sp_Q}d6|d+pzMR0PvRLA;JFz{ZmA^vL`y6g%5=Mo3AUwM}(UHZSDLJ{lfdI zw7dUvUa0yBHH>Y3kNy4%m??nRR0a34aOIrKQ@HP3VySMB`g#QdpBmI|rj@I% zXQ0pBEOF$>8sJ;{strioOR~ClZXP&xG;;O-!tW#1plWz5-{V4<)bR(QL014rmJaP_ z`oY@9?T7E*=KJcW|7AH^(U(CwK4?3$vR@^3Kwt>@R=|j6qLUT5y08L*DR;xp1Hhy! z|J%OD6HyB*Br9bi6uMY%=$7NK6fLm$N$k)25NzE3EC6IpS9^l6UJlCk3mm~^g*-3) z^M=V1e}wApX5d0cq7#JyXXz*snrH}YVP%KD4#kTFh4r~LKe=Bh=I*(9f8l1(q7EI1 z#Lz)xAEeR?6T;Tue)61mBQip_2X44(nWC`_jCD{X8ji#6w3CjlX1oE_W!@L4l zBWZ^EDSZEbI(p(GI@&-17UX2p1Q0dB9SxEv-!ciK=O840 z4M-erPOOE;E6loZQ12rCdpjHOKi? zBFc-Rm%K zY8tmyWL{Pnne(nS07ydsI#$4n1gPo1>OE^LdK>^+i+!>&7oj%zWT!dVfg99|24xa& z=n6$T>t$S_OCQjU8LG&T9{{pu{F}zF#QawpAN!p`QmBkZpG}R-PU|J-^in`emTa(z ztfZe=jDYN~2*2-$hvSqK7%hh<#+m<{!_%#z#7H5bEfI>`sY0Zj=$4!ubUrYUt@|y& z*)9L2TL$;LxRy8o2Q>#m>)=mh>EqqlPeXXjW5{H<>rrNTV4A~Fju1M3@*Q|zmkmN2 zfgl&m#^t-XWi)1I)HKnz-b6Fv!568KmKuPIcrYW8HtGQ_r3(u?n>G3=Z(Oe+rx1|5 zg1}Jxu65J@R(UpiHur?Pa3L@fl5?89U;o5@n!N*xit$04u5lII1>E`t$cf@)Dx4R~ zHcEn?u`W4|V|o|&%r78pIs3GBfAf?5w00+=8;Q^|t2m|Xoab&?2cHmpzlxDmcxK^K z^=^0+4xv3<@Hg<8dSFH_f-x!P)V*iQ;i0+rl0@__j)7e=OW7*RuBu2KS>|-9%$yeZ z1AuNPIc2{q%e38}&8wvF0UkkVkvpNFvimf0r*ZD9g{3N0In_T)`R0pbe!;6G0a0kS zn#qb}WTl8?Wm;<-nvzZH;-OTi&{rsQrIN-gm#idExeIy=i})qW9?6zfRaJ3g!BT6i z0NC^C*0PyT1@)f*4A^l0c~JYXh{FWvVGx>e0&H)URrRxUXuf!qc7#W*<;Emla-^p% z44XziXTUt4bIf0NNWwHGhfWs<`$t)kGR2CI>mYBn`L zc`6wQ&kNka=UbI5@n8%Jm_f*5cuu#IQ+54!tpe&F4-DJ`gSga^@NAwj%zZ3i-=TH_pKydo;=88} z*Rgr9Rpf1I{jK5J>!_B`t*{p!O)uJDv^AJNvs+r*S{|V)pTwUw>+9TVH21#94LyPZ zJ0hFLE;W3XY`JIC_QD1rO39T6GPLG)*t~E1{q72k7ObBD@+|A2$D~YX9Z>Gv%dyKX zljk}-Z~&?e07qb5FYK7#Y5MW21wl*HS8!%HkU9aqMWstYwT;^})t~F2{_dnEv?_$5 zzVmh`&cDovqmio5>q}7157B5M$j9d8%biYI7V^agV1#1@Qh&T%26&+kB^~_g&T)_RIfg&Y;0wDrT7V97LxZWk@P*EXHi~E> zOp6{BygT4ZXqPi(bgk|)Sm^uy9ub6SH)2w{R^E4~3Q|)N5 z1x%@qw%CqNdm>&^S%!(=_qOA1?SM&Z^g4-dPI(Mm1kJ{mHQ+!GJx7SDqXi3N`<{@H z4VGQ(`1kfvgFG05rrEEI!)Tg40qswrXjP9*RZr~Oj%wqkw25>w8UQ*9PTAqhkTSc(JD37YCA2K57s7uRR}#&btuD*Y3aRbsXsG^iQrHoS_>O{)9ADs zhlhb50Z^evqj>2VEu)#pe2}y$qi4sA%;>b_?$r5^SI)ucwTH7X=1zDj15s@%+-}Cj zXf8h=tV&^_HcyxAO?&N4NhQtS@oa$>0hF-sPGfQONSO58ZM&%^ubI|-uqz30&J@mp z2bOkB_bkrDC(WZf=rPu(zWHVP_q1Ldpo@NurMu?!zTnThHm?~)+01h%-HhG*3*(rD zUb_WjuX#zSX1LIMvKP8G1T95=*Qz%2espm$WVw{gvW{6;U0g0TTH08gmd?Kor^)pb zOHi6zPe3CuGfheF5A(r4)K(Xh`p*JW{w^+m7+o&0TQKSXBp9N-(U9t-6&hLp`X;&s z19)Gv*lM)OW)E|jSlsYnx%X^U`RVF<&C-h07{VUC9*mY-`9PaHV3ql(DEsk3=o_&* zl*-?=RkfwHKlHTC6ZGV?E5_(z@(OKP#i7@lp1{Up(we-?M;%X=VDwTK9@s_Rz{#v% z_Xa{G(OoN$yVYnm(x&1hIB*F}v)Jv@KS=CCGH;^Cuq6D6`bQ_34VR@Y7i@oU8o*0n z!Mk=N?eE$+1 z#2d)*5Uu!Ur$Au0$bPpZne|c#+M2vI)w!P}P$K4t>Og<~lDy}&glZVuII3NE$PdpL zM$0G_fNjeD)EEHSMV+M~{`k-6^Pj&Of1VY12di6W`SSUL09C_h9qt1_ zIBcMPux!`M;?3Y<}2E?j4H`6Zc#1WN?-yP_rTRz^*1)v;dAteGZDGK^B z6J$UgDuvnYQ%CcznD5sf82)#pp7iY$sh8b8Me5OqvVzQx4(L1*Rpilkq#)2e1tgvd z`0^L(K>gl<{vpxz^_%zSF5CyODJ+^{AMOY>`~2zYUzh~}t$6+@P#t3AfbJk1#g2ag zhaEt}4sy{);a4D*fU7F%1~MXkL1%Pr;C#@#Q%ll1>|{e8jod*}RTpRR?~K%nj)G!ISE zM;~GFogDx)Rst8`lrY>KaRXl`8#^(a8+HRN%qLG}Kt(oMB1$Hlgr5k2)r{#?NSa*d zIx3o1bu~ow(u8aJRO-}My@)~PoM3vb|^6}wZ7;6 zkG;c-uYGju12@VP9j|lAe=DqsXHyJQbJdd4xT%35glg-2HSx8J4#hD__^M6xVi z5BwfKFeiH{H=KLUAjbRB{PEtQlHWkJgiVxsuIYHW8lN1f#*H%D#b-6GQ@Dni2q*8^_NM0 zT;>c;7cI^>LLV$ZCLD)ZsZ!s(F?LKL&u=2k@2-3C* zMLgKsK43N=DU)w@E3`eZ=yA-$$-?ye`BAbpw z+%pvkAA0d#-&SG-bGmv(-j_$-nz<69iSvh!)gg~bsX9CQ>c@**tJ)AT+UUEH| z&3H^Ql3RX!_QjySO?B41LGSHsfVhqKD=V+YyE83b4PLhk?-WFMzuj-Md26%q%tyHV zNeT0Ztt0{Z9&56!I#2tlau50VCWtwadaB%CtGOVqG9~knchaTYS_m#~y%gQ%_2y%z z3aB-o<=4GA2NSmLFCr%kB`?&sgRdQW$M4(^Z=6}MU%vY&;^IWUa4yp6>s)u>);H4M zpIiCMtG_;#ju#)6-TEQ$z3eZeYH^(rvR5i*`T|`>dFF{-A5DdMaP;F<$umyNDUha^ zq;asme;NLBAgJ9QSW~VBu}*nt`19!x5p;bS-=0d+tx6MI8B6(I10U|{R( zn|6IoSi-<+@P__MX4y5Dvv%Dg;w{&+7kcwz=QfKku0r4s%s?0Y&PLjv+_+s0XwIC`X&Yxd4=wloi?9!L8Z6@{KSFd=Z<<`!%2Y;~FX7YTr3Sy=jp zstW)Sg!7xyyu9Fe=PLg%=985*x=*_T7f0zQY0mCfHkkV2GYoDTDEA4`d+X;UMXThQBht7A%_`-S++=eu z-AI@sCN*H(%(a5vR)s16TJS4SZ*w#93sZ61pM#=(J~J<6%xgTJRMO4qCgj^6mRr=g z^W=acVUZVAQ$@H@D=N9V!!Iis$ZOZ8i57ig-z#P9b^vp+^sU+!Coj(AYu^|qfjU}8 zZl2fYtJBq(kEyG`7%nxadd&mrZe81wn?2}gs_wgP#_um8 zIE_quQ3}CMkPa+^;m_Wfkll}aNUlB*<39uq%An;n@ukFF@bAUI1g-2>} zPEx#OdY$Y&Ozp=`vP@QeTYBNDTto4qmz4ps$#v;P$G4O-f1K$eXu7@#-YkPq=(-5S znSXXSWg`M#^WjB+!QHnc#4imK>=p9fF)!!Za*n5U4*6w2Mx4JRO!w$-cUc#oD<&ok zNV0d5Vas>bxg0dv8BiyaqiLc)kb`8u70>fGFHUgf*GSvGLPZ*2LR<3Vv{#0FUP$*g zr=Sgwc_SX(Ea{=Tc`H@vQxuDQn1`L8_4`M#Zw%JlD6aBL3%gjw=P6$MeoDn>=_}y% zUu>DSmac(kX$P-O`5sumpf~)kqB#Q$PR_jV=Qh=qP84%|!O-U4V@Rr^aR3DpJDa<7 z^xdzqAwTj?^AyLBViSe#?-#q>9?luvwQB@493{awz_E4ScnSxL9mu0&`R7N+pG4W4 zl2=#2IzL*p_ED)Zwqt^h&BJ#S31zC`2!qVPJ~#C*Udz)@MiI&_<6ehOaJ4lo(@z;) zHJYT~yT!9Hz>(z}URs`yFiQFPrsydhgTx!IH%H2YEn|nZ4;DE@3v`)3sDFR$j!#gg z1ArxQz=rbtJ>y5M%JQ{_cGGG*R|v7ns~sV zQ^AjS15Y{INnAG+>zo!@%^5thEm(+5#~)Y)zmz-t{?I@fyL@!~HFF?%wjd?YzQpmD z8};fBd7r<#SIkh^*!^p|)X2{$>=)L~FBdwqZp8euDYqu8Zuxb-PUw7>t8TY)Ig;3L z>>**@OLu25MjU_z&{I|}sI8v4B0LD;brSa{T-mhgI*h;`<#vAhE!l&D4YX43oqGB& zbICD&TNEZp_h(j(HwLQR?xmLr6!u{3&jfWYTTPp0C{h(cJ5lAeEe9#f=S@IWl;JYF z8W>2o5d9TlT+BayrB+#L-<`TdgS(H?uAJDDpy%<0^ zc1=_8p!+-@1xdLQ^9(L(+p8O?VU>4Hzobu16>+OM&B*x-w=R1b7Ziigz5)SVk?PkF z>C=Gp^!i0g_?KF`UbFJo)GO&zD(SPOMBT0jXui{u`H-I88)rv~z_V%J;?s6IR%e1} zvg0Z(h|!41YkGOFtn+kinzbY?S#N&S7M_Zdc0yiI4)@{HF=cyo-EhG9SUvRUReBXP zAWz*rxYxZz%VsbtE>G9fKmH_kY5+Q)Tt)`)B6LY?TBgC_G9r2r5Q>3>PGt}gGDKq26Oo%nu+69cx_Cards|kH=IAWRhHy5EK z0Xk0&A3q+M5E(deq!0HaU+oYk+5`*@c#hf<4dx<8Y2Ix)0l-r~I!w`kyT)5$qL+3@ zmmy=c*$ECR^nHBPfTuxI@bG5vDDC@ZC(Bt*i?aufck+yd3c=+7;CU)>|6uI%@eq|h z{9e8ETBIKM-f)?%iJj{>)N9+@lciuG&=n@aL1T*bTFhM8L47va)=;4Gc zs3MB(o-u6@;(PGa_r9p%l5i8+Q_wLPadetwmtz11N0XStiNExd1#G4m%ZNK8vi_FW zPoOR)u&C>{%}`qk^vFNn#1)9F!7;i5W$wju#e%WW8?t*Q%Y0MBU652R^w58F)7kV# z@RUl_v^8nCjMiZnn(WXB|GCLYClL0JnFhmkA^xc|Xn_8xtVO`6mdhk8#8f%|hGl-# zJ7**-I-KYAYZ~EC|8_z+fCxXDRz|!P6g86{jTEkzwsw7U7;Gl-p5GBWWB)m;2&#sT^kw>a+mXM`?a!P(Vp%hTdc(wjR^X00pcPOP!fZqjgC ze*L_?_cXsA98DnsE^^JmW#8R}S}?KSlw*IZ`Dge{X}v3c+#`CFZoL|*E@;1v5Nro$>AX*T|?b$$)he{l9R zz#bgfeRCRM*96A{FwsI>AaP~uFj31g2D1$Q>Awp=2vO0QXqnq@<+?r}pC6S!I%`h_ z^5RR!+d#b3u>5v9I?JfWJ#bUV`)25DOO1`3sM&c5Xp})TH0j2Jeye<_ZFfHOO+Kx* zN1ZzU6W1R6>-c-zL!xjI>dlLd{GgJ<$h5u1baSf{jYShVc0fz|MW+g=O44=R(Rr1< z`6;zUtHP*%F4CU{*|NqD{uTYB{NrJk5gN^QL$o6yt#`^8%asNjrWLrV?4_GT%MIuP zg<)Y+&CXdldU%UgV-s!i>rZ2B>NWBlfR_sb=2#wlyI7)SRmyOaslw(w#s10r_qvBR zflnN|OC8>nLak$$teT^(?jfB4G}9kw(I@|}=^u7hE!La1E>m^|{AzJQ&ANo&a`=y3 z#2-_EiCJfo<;LE;wX7XOv_0bn9-wUAa4*ad2c~}`D~pRTq^Z+hNKkVa4Sh!eAlw|w zU!E*1uG^)lIS`DNFQTm;T!=ogBHoPma!ROi*jmIhCPNwTM+Ixe{%_YG)qYxLW1aFW zqrtRcgo#_yiYqAU2Z*tCNsa9R)IuoW!?;sa;2#?yu@B1oD`(1}m()K@*U;aO4HI68 z;oVw_c|qh|i4l$sV_8h+!$7X8uSqAba=)@0+KZ6Qcwg!=e};r#yc)gHfuD+UV#@e< zrtG7o<%S0_Vsq|)oBj%Rr>4KF1)#7d(YYBYjDc7^b-CKOZu9pRG;}S^d_LSf@^T`S zwh7=D|A(!;wLs$y7yFG*42=I}`bSA_Jo`VU|A{1nDi@4@0!U!o^ig-_W^_1#)UFnL z;lo9v-#}DBM~*+A+zC`)WUSo`YTSIN4ZEX_{(n&axh?PihWaytP}Z?wg08MHp<7n9 zE;9SKARNnf8|xRFr!I>ST?zJ186TbOZ>M!`KD-@u`F8yO=&?x}khuTRW2;{!F<62N zVcRj$Th?NC;%m43HWvNwha0@raVm^b8pEIAaASOW$HjQpMH}X9>FH0zsdk5%ZiUGM z!cj+Id{iJOCHx#A!hQ~FRT^%QMYHIkO3ffC0#I-;jCOgAk%$oBii-Ts%(oJD#xKe+zr zH98U zKStf7U2KnkQJnj7T;}noamVNT=aX7TV2ufQc`em@pW~({B8I@7`DOdnOZ#S^Vn766 z*H`-SJ;qHhX_;-`v2`IpxO?;D8NpAFzptHC-KT!K|5dzve|u~@dJ1$F6RuR@cdqLz z^V}EyWval5&yu?H`Eid02GH?~(KAXPjQ_6ng;M)(e^V^*zKMn1BtUtXp8Hlra{wTh zoBUJ6);QeUiyZ-Bw-q9PksiKd*yvjHfFYs5o6Qy@2%87C~ zvr4>o1jf(?>bCAVEc-it_E#J5V*E^qEpxg0bV~yV0zJ4NN!voB>)-E9uzcXFbN8EW z!GYKEeG7>r3)=WjP_(IRr0N9F_w!-Ql?NyOg5Q}d?lR1NZT(A#Q(Qqvct6xZfUwa) zjqaf__dL4pdvXSNl^=xP4fGyxXcZ#fnF}`>B#LkW9R~w_mjm$<5B&c5hBtYj+7xhq zAiz212bXq4{@(!F!lF%?K(3P?mMQ*voB{e>LE%aP7Oqg*g9SSJRo)y02!O!@XnY^= zmMhSQb`2g2x*YJki4%5t?x%0ck$uy*!t#KG&pxJ1hsB(C^oe)N?nJ>he{5y^sx1Fi z)%5H6gP$3nf7OZ;ZBBy7DXHwTbiRYZzOE0OVt(gkK5X6m*;W9YT0U@ecz`R|Nfg|M zO8~QP|B4`jon?;9X;bn9* z9U-($>4(GlkfMQa^q;@s1n*&WLFO?b=W4%iD*j9x|3fVJ^5j8qjndIz`SGew+DKRM z+UD`tozSh6fPx=~g`58O#(f3h2>we5W)Z~TAWRa;Det*O73zy+5x-vT`sIV>%{oQl zgtwDtUYtt${_Ioi`svjct+$4%-VR&H-Flyz4Ax(%7Bw&eBkDlVR!4FjQAE|CCd=Ih;OUo;@@i0C}_)yb*OeNj4` zan7l;TJOT9QNC*6UwZxet;rJo{1a_2+k4S(tE^utx;t56HeN?QCcEzZys$NarYroo z#k^+QDrO>lV`zJSq$Btb3z^%JYW!3qibc)c%<_7_?^nZaXNb#(`jEDwarW&8g5UEGItU;7E47Vl+Zp^euDy8sV*lg{ z5tUk&Z~J`l$>Rrf!yB1i8~Fpcpv!f$xp#H?;fcYx4q&#=py-`5K{DNNg z%>dN?;jE#uTJuDb@`mo}UEQC9CzjC^RWXrN+lyamH)9Gv#qwO4Df(f;Z~OE0Rz8)^ zK9PHC#%pZmS&8$G*n9OJ$1W+x7oDVkFE2)rV?rbEiM9S@-hKBV@$h_$vu^^{fK@KW zjElP6MVG{GPe-}r=oOfMH*#b;mpLEw{OZ{Z=S_FI5B+yMLvOpK1%@MM(!5#4wgs-= z?^EIM$@3(JU z;ghAz-di@H$lY?{D}MEHK6rO}E1K_iPcO5E9{ccIM?hagw{z*M3-cmdg^?`Z7X{ z&|Oi_GX1?bln_e4RO0JdY%&xctRbd-fM*hDm7q!b$f7?J$ouU@yU`=sOVTn~z6(1r1D4<%(jQvT( zqPS)RDX!h+P^J}^k*w8QqAf2zM|-8M%dcdDJ2j7}EzGNhDt^qu#s(%er41a?g6gz| zR?yecvy04GCoibga?h8ypu;s!JU+h^W8jf^@KNmN<_6-S+8omEhBW579{O27zBA5neKj5uz?q&xM zULbmnkRuXoSoF;;`1_>0HKL;+EskLRnM}S|p?Kx|xLKFXXJJc$KW-)5q#O+b{t==8 z1?vNzINf+9!3qe}-~ef4yZFwn#$dK-vVV;bDL-vNeyO0Mgd?RrGod zrq@6vTBC3)`_8jU$KV6zA+g8$GJ((TgQ{E$6V6-Eohb-b7j}oIxYp61S7!Xhdail* z?Yn2>oRd#mBoa@YbL?IDcg|(^m#kdM!NINN6)6)MF{*bwHFSi4h^lR4Nd0326=Ju$ zba67$>UoWqMeEwVw%b*e|KxAeN%B7vILE}#?_b!>VJ$*T$Q+-c?aZJJuitH$9lqpI zGt3`Y5Gs;kIbVA=`CMuFMtDwb>(}~+(QW3osZbf_8Q!#_F4f}Co&S8#-6I_a1_*{u zveVyb{-8N@_GuyGZ?Y=0L9q4<9v8^&>@p{Q50aGf!5e1)v|K29L1o|MAk_H}5iGFWK|Y`iDj z%i!QuJ)`)rLu<9#0}BVyVO}ch)a|TzmK=}mR``G)j9iRIx6=;`cS4i<@LoY zOd$tRH~#6O<7qB3_&%Q-n?=!~EUaRD+s_qzW{XlE^)W-=?vCUC3Ls63=ptnMvAJ zJBAxYs64Z9P(`@5Em|jFSE0lA92Xn-?h9sc<*LBp5ek+5IORi|@Xz2M=Y`$&b?vUL zb)&y`9_6P7^7z{`&muIHlF0&;X=lD7a)Jp5gmWF6D6glYbw%6w$#XVmhKK`XZpJYhiSi*a{nTB@!69` zZrY2J$urG0BQi$J&GBM_?WAG^yQHl<+KT^RMdYfb@j1MFa?g#5RQVX+t$I26akd-kP{JN35K z0p>87t4GP&EX!xQ!pw*Wm0c5#8YEAE^xdZ(%D>E(3K3Ia6&+me7uWUH&4r4P0P56m z{`Lx}f_E6b%YraRJz0z2y)L1h3*(UQ1Lo=@3r{)XV!s=3%q9+Mi~eQF;u)3D5l6vj zW7-_SbK4dz`s3;0Z$WT>HwDMxFiq87UI#q2FIwImPPb-C)19cd2U$N zdV+@fA8CSZFSnCMz?dP-uS!f;!}oGd_*FAU?t9go@|@VkZQpxO?WNZR{^$x!BsV-r z*nWK>)RfWwfS!Ld@Qzk{WU}pUIMawkAN;#>DZK!D<B?_$81zqWZ5zmW_FED=g? z83w-c{3@s-mk7PlR3g&veczmC=1!-A?>aR~+ES=3jr^f^S{Dnx^%D5ztx4U_x95F+ zpL?AIYA%Kd$ifUK0CyOMIT&OHKs>gKquK%4-yt6RXs+Id7scE|b8|8oLh;ak*9aTS zU6FTl4j)^O`sH3xh)j+!_LZVum3b`{-`y1foq(q-+Mo1$W3ZvVfD*A|y0ZRVN zsKcp9_BW6JdHT~Tg9F|XmdW}~M%ZMa7fxM{K(0z^(==2pdP9jE&{2~En+nqO=Aw{HP2)y~o?)~j8-%M$y zaM-n3$(LKIg2R(P$FBwr%qc0|pX^9HPxvLI+zdPchoi$yAsA60mlh(NLgd9R3N`4` z1`u#j3AHySMiwtMc0COq`7MY-M6(x(guJ~Iaz@A7OUa}p9RMWK`zZaF$WAbqp^5C6 zFq+8D7{<7}0^=v$djkuE-u8+u3;tUU+98Ky2nZCFsNao*p#k|fBc^`@x)0zTWZhh6 zP{9)yPDOScZYv0HB09 zAjgRWdm{xNq!I068GR?~zDN_EQTzHH1?5?X2Cjgz3t@OFE4~nxy#m^7LJ#7B{1_{9 z2`8KTUUiMZXPd)!C{Y-4LIDn$Lew-3Lj|RBrt3!Ec8w_1@x9ax0Af9=5uk1~$Xb?- z1^{;hK#BL!TbR3t*znXJ?(_E(B%7mJ2SEjdWDF5r*~3;x;!HbAe&remMR*H82%)1w zUl%I>4~TsNNI&_|2IuvA0E&F~sA&+?(j1!*klbNO6OxJe9{PRd#9qt9H!+D>2)9O3 z670XnW)OlqbgF&# z*fMcoEAX3y@6+`EQI&T9;%Q!RRE5v2n1uM|SPUkW)e0#$ktIiFWB^2{30?QH%1)C= z$q0y8owogECA7Xb<)38d0`Bq8^uEkZgkCRJ<4JIlr^gd?kDC4V6v6Fk!^SY zWIK7qDn0XOF48(=Bktd@98hTZpRk;ShTs^@CrCej=FsBv+j02{B*bwXl&ZqkNMVGu zK7$SCI&bB^k^ugSJq^om(sBOBQ*S3j9b;nS24mmoWh9?{HkXZv!_oJX5Q~6BO$`*J z>Y3E`&E-$eJ`BP0;!gAO6HD&Xyqp&96QR*93AZn$78Dj}&lecPml*Ic8exj$Cy{qu z0K1q{sn%!k`P}s8d^inKClqz90$d6~x2&Npu4Qp8S?(v2&pfM2^3avK@z6IIwrrpB z2UX<()}>xQpQRmLp*scCQ@j3c%aK4J7ojZ9DqqXJ9Jub z$}nSxJD?L^eqFLO&pNxaFc3yF)XCKmU!l_Y3WO<~w=gZyE#G6fz@W9JG3dDx0Nm6H z6^>)wBEh;Y)o#SsX7N-%bO$iFqs4Hrl3${HIGAHR8c2ZE>E#N==j&g3K5zYeVZP=a z0sIaN=FDYnC6haTJ!iW7GH^cQ#mNJ-KL!$ReV!2mxg3w?AT$2cTBhgzB9(`n2LMBI z84Pn7ED7ZAFHoJB23dpJ&Z=kV5iV#BXNoJD5lwIR>jEu1zifl9B{E5(Uo@z^AXU`> z-`7ayGUywC-eEyXIE3n|{f%=CW^H7P-{c_cLKsn!mgy@JiW4HBiiBs7ZX_?@x!!zP z{d|qy`x^cC_4f>#M2J9E57r-3O*eL$L_1=Y?FgXs`)q@PB2w@q{^FLZVml3($5QIJ(|TD6Wh z1pN!E*Ad}zl$yJ3^&bqHP<;KUcDh^=L$eiY1F=`hv)3lLw`;hm@b_~!YB0E{xya41 zmC%fEL@iP~nr)g0)s*Y)bTZfhvv#^W*jF->$O%$!JEfo1u)FVfKkDI2D2-Mx%)E9Y zb!w|ixuN+0Aj#MDvRF{YLXWR%zu7S;?-&%ff>KDOccTt48xHsJ4u-ckT}Lq-UOw&G zX^AkcYe%Hgr{G%%?QI)2o%d}AWN_eI9Jt38ECYZl_@LUU1H)3?Hn01`>AOA|plQzf zb?y;b<_-x&k7I^f6DWSc1G!XC_v@ivTW~3<_3SFLk@R{rzyZHm?$=FAcT6+=8B_GM-!^B=mQAuWI>poz&!Jljf%l}QR&A%kI}nj}b9RLT z;&oFGCZ`P-=MwkEc2$QTsTMFc7Mrb?_8WZaG7p_#+CU5 z{zc=(i5;nT?S=F-58i|P9!68tPblvKs3W3Y3u|8U8zJ+e}Fos&4ZpUY=wYCvEVy-EQK3OHGA`4{><0e!ME9aj=azU z!RUH2G$LdPlDq^B1!3&LNIRB65=xA5aWd({6vO)=!*>u`5RY5^kDiXk&H+Lf1Uf$o zhJrE)AUtV`vs*yi`@{I&yzbb_WX%ArhEI|DXod&+P(x`;VoI?~gQFiYq4PG3A2JDa zx6z}b8i0F+D6P;9@xMztopUV&)V9_~oIP5$Hpw41A62t13ix2Rzb=jeJNyL|=CN!Q zqHaG$MI~>EA(cXDWjuNlLM!8m=qNH>$>PVrvCS*{>!D-d&`tzHAxk}eOU!#C zDs-*Xe$AuP5xxr3m}u~MiqarIgw>%+3)h48KRWzf50wGSuYBs10gL0Hl5(i%(5*`Q zwX)=mgF<@?hELFy zZa^_$4Pv;GE8#O!V$A#@)rGy?2H4V%^9C{C;F8D$;lx?y$8TN~RU_mpjmG z=r6Qlo_YDSn3vYvxyTGK#s|2(Cr+9X3uA;mLcvU^>nGmS z0psn`(CT%AF-Y%yn`{LcnUv@|8wwX>_C-rkRkg+l{h7yO5aNT z8stEgb2yCL{{T*<4T*fS|NQNi;DPew`<2D--sgY#d_M9M{FzY*+FfGL3PZ_u{RkZY zkz=w2f4>&JfWEx^4KK*nAiEBC1SXIVjL!dfeDW(}0-Uh|(pB5YN};Wk{2lbq@7yk4 zxX@=7xnEHZzcf~9;W{9Xd{E2uJN5kU7gv6$QI8)ouv8cRo+1_nVf;|0k)}5IjxlKQTd^uRx`!hmKch%Q5~eX`_EKq16;|pWZqR^CGc9 zbI+zGR>7|Qb_)hNgbxG0WcNr`BiC&bp zZSvn=8@e$&R?tKql1h7aTL-Ck*;*rO`fQYv#Uiyu=&E zS3g*OLc|La!G!vfXhaIqW~>;`+QD;UTZm&~#84f%lwfitz9eVt?BR^Zc1N`K!VX;G zl64tu;l9l$4Va++)R6H+_C$k~dmU9@Dy~ibDxJzD5jeAhziyj=(!5CJ+yTpDNzCeR zdL7>71&Ib8M=4V=p~K--Qu2Kt4J9}jdZ4G7uZm~2En7HAAX<_A4@{*(K2|^kdp!4q z%D=v^$W-O^bz+SZxZrW7{C0`l*rioN17pd*#TbZczv1f`mP=LZ(V7LS*qfS6b8ksn zAe`BG&@rW7UpGyymP1bo*$p)ioG*NXNz17g;xLn95O%-Ickb=o_XinBMW>GGi=6oN zSPnGu&)W(3gFkO?2A%(TYpyZTjqRP&?}mwCYmhp}JFA?KL73FDd#=`2dJW%&Zsu1> z&|7LdA1&OtVDY5br?QpfP4r<;PjvFFZjk-ewR|O)CswtY;NX;7P~Jyy4z874xOyWw zxvxcYHCpEBQ00&?OjD=dnLWsypyxS1TAt65R3*><>LlQwbA3QPu!CLM4-ay$7>q6W zR&Cr9;+saRy_`3USuM%ovKQ-^7Yj4u4f)-xRFkFlGt4VY*)!kzrXpLUmBP1T*)K6- z#5~DyfyX`r;t$$mm|mCBc{N@B9PAagx-rOV^vSz%3M_bmlFeaM zqwrh(xWZobQ^gYozLbU8AyK8j0UsHDTKQ!w|2PiZ(ps)avLkxO8e3kf$^vy(Ha8#4 zDqZ)Xmb;bPxw|<%8ti|5IA5BSj38?Ien@RP3H|j6Z!dQ9cIAtV6QHb@`Pb!dB|k`X zuX}>bVoi4xdoCf!2ubLg4z^dnZkWYYPQex;U5HuM2%0}p~ z^_=EfzT+c)pD4h|<`upV6VZUSEN?^T8rvXDQA1ql@Yz|d z1prF3u;%M1BVo&+z+=4Jv%WXTIA-)wM3e56NTSn0 z7!wZnTAt@y)+;Ho3;EfGY=jqSj6a=$8g8Q=Co_}j2(y*Diqz5UYY+~$ovrx!sZr^g;k8jZV5w`vt!nzt(isl>q8FvQiJ?T!6pkws~F9jrLd5W&ew~GyjL` zf8hO@ee7f38B3OsrAXPvE?Kip*0Cgo8j2(|GiK~-i>Pc_QXvXSma*@QED z&&T%@wLj?$Zt?MVP9fP&&-}GszO0Y4Lcy`L`y9!(iA#6P%#3-DIoGCmsQB_e!*)Lc z@{S%eJ6d_)>CFe{mo@cqtztXnH25`a-M+sWR?Y3|lR%jsoMB+@^yIsf*Qz@(81r=Q z)Y*l-I?WPOWq(#+PK$h|?a<}x7sfOF@J1Y|PVJHTzdnqMkg9&kV8S)mXFo5zdgIKY z%HQnh3B-rnisy7nzgHscH5_k6##G)ARf1)Q+yUNIJ&k!O%JygNyO1QSJwb2ANjj_p zIAeiNv~Ki-6$!PjZ-R&?li0U>CYz7eZpU00DM{5voRnNvH-{}Qq~Zxen;eG7geV`` zqWUIXMi0gOIni7AyjGV(T1brVd|{ecX*XA^#RDvErp6EY8i-4^LZJZ}!Kof|=l1*7 z8_Rn~e7eO<5qAo$e0AKex7bZ6@+ZAmqJ$+Kwybe|0RAnI6I3{O(dJ4f@5ZW3`w6HT zHN?at(@Uv;e<*zUn3uWlD)1~#1E=YJ1oAsG&CyrrqD0qI;XfC#2ClF_xl&4dR!dmw zY#r`nHnQ3^!{*ePdu+NaVW*Qv>>dPE-=-Ev_i~^oMY2{O6>DysV|ubN%N}yk!+o zAS|ctw|8iIhdpp{|8UZUTz{jFwP^kR>T3y|*1->RUfPCOtMk9Cz;v?LZX)8$y`Pe+ z`d^yoe^Wlq4nY?+9fsUKYLGq*m>FVX8EzrSMPxN$57;-)j@{M z-pCsxxx~QGwdp*zO8s1@aD?7hU&$N&{hnWsK0Y`MSgw5d<@pQxSQE15{k!fRWKukn zz_mz?+SOtFSbwZDt7@2B`9k{F^!MlzNhX(1$lqkGCk7qh4|Y$t^xkOt_N>t$0rdPL z?57*X57nFo9A~2$WW6MWC+CM@At!n;OiFL}irzP016ab-S;$HFjK1qf0~Yrazi+cZ zwn6Msh8&Aj;5P>RJ2vf?l8Zv?=1YTkW-8sHmGTk6J(Ivh&}4LtWCp10G%7%D0v3sG z`%1nv!%d~8R0Z%bq>XEoAf1tj#S_8UST&P2b}J)xKC18laCHAo!h#;-FrS_a=41}D zm`-a`af3iNkeo;&c(q-h&sdghC}-8qZlV2L8sM2}7GrM~Gto`MA;>S=xdOHGKQbHD zFoS^(b#{{zRO(lq_FY~;A5i%*+G;9WZ|VngohIMA-2pg21U@0%&~a8K5bI-Y)={l8 zx(2`u)Z7Z=Slq%quEd#`N-$g0JEeww^y<<@m=0S8bvIfpVd^OTV0nrgs>0bZH_%NMz+1X!m8Y z?#t}m-YM-I>;J^lvE9ruWgJ!~4MPA25F=8$FDL_y8#P@91D%=~FX?A6X6#+w>^*^ zUm7bxe3h^~1(hl5Zzw*MTx0Q|ru#vSnc%)DO&SQ5wRoi5OVT~Q1X@A|68i2VH<0dC z%+I&#Z|r~4VKw-$JH-*=1ym{W^)nnyPS+gkd#h_hjY`NIfxX+1^}sizMUcg^hdwn9 z3e;QWl%Kl3IH=Da4;Xil*v&e0trw12e>`SA{xI?D#UWu4)f^_P|2RDoD3hWd(a`ipyh75CkD zha#YAtzzA8Wo^cE2f>=dEjmmV9}Y{+VY2wc${Cs&G?tR#_chjU8f|9p^lGYg0HblY zWNlya*+W!DoV9!7m@Gc7onMcY$>Q^5C!Vb$Gv8_|-W~Z-Jo2;AZdMm~{C_Vv!vRST{fe))N*RjtCm)K3$*l(nClRv3{)EzKT8Ie+P z(9pAsS3s=#jW!7h2Sg#k3Q+3;hf@`2bV`OhLU=HwWc48YUmEWPAp&*9{C&X`vLk}l(%Mki=gjpqQdQCR1&a|Hhq zN2T2HhVlV>yE8j)$2|@?dM!D7@jD%IA_~+ChRi1XO`;bh!f9k=0wL_&JOA9#^SR?< zNYKS4_0}0D#?dBh@a#KhG$HknQyMdf4onhCn41s;+k!;qj2qknOYy*B!zSWOY_Jw} zcaOr8H^-C5C!?3JU^I|En_wG2#G5*C1`u!OvbZV0v(8Q34V#LKp7`-_7($0-y>vDl zbcr6Hy7VnIuY!eNA=cZ}384^6%4Kn;l91Wp+FVkJ2R1Gkd+qM9SVn(|%-BOFcD^}P zu{l*)!oq~_ncpJ_1-_EYay@|{jdf%8fgT6HJ6Ymroc;2y4DeuZ>dm-o)6&`IrRk!A zSlsF;e<}jM!}48{bLi=}!05CHJ9mw!hu;0v84o z{1vGpR^4TZ$_B?^W;?8BJ3VH@!aNSwg6_HN`chpQm!_L$-QOwv1@8I72L1x~0gh<| zf?^LO#=>(f7rg8-n|03loeIQU4d{9~;o0;db#`)a(~Zug?Sbq&*>w$ zf2GgueADcXNgAqC~k=#3nIW*k26Vygtj7u zf7<7CGv?*=FJT|ypN4@u14Iowa0oIEhl)=MKc5oglj`i5=IL&XHLv{URJN(oQls|wm1ENBw1(EI!UM(q)<@Ny<<-_66>c zX3eI++Yxs8G3xTOA^g`mhC$a$~ja8z(_UF0nR3$V~78NhC6$0K08isEcWlVDU& zyha4+2K{<|dW2l|zTxpbBlEp$=8+?nzEWx=WdRp(JyDxyLj0^3jBH?ptTg|MxOx7h zGYP!@f0D;YVMk zLJdkKkMwDwexbW=&1DR102Ktu3JQ!nbmBT$W;xB!C~G6-%%RiwRDFkvh2`cq zcn=p;=wK>=fVyQJn+%1q!}+n1LN7Yd-JGg9jS-;G#e<~*Ljqa$)7d474wS2Z@XMr{sxpI4j5ZespV>y*sVID&fT8Cp<4mR6f3Py6FAZ zDfNeEgTb+475$moq(p^@>*w=sc+ywUM9z`;!aQC4D#Wm>wM;NIlIZ7u|I|-tNtWkf zSGk-6y8@#2Qt^G@G}`&_>!m(30xYz~&@j&9kCxa2^drj5gOI)xk1G`yrq-WRS#?aR z6Xl*MXAYiB@~XN2n!%c zxl`13TyVs5JMmquuQmEHmVbkG+Zf4H|iUSu{!f>u7vyi1I1TI zSqSl?(Z8&zs`5bl=eR@dfdUD&bz$qs;p-kN&A*4Nj!yZ{MFohRcp2?CoVj=W+Nmrf z_hTPQ{t!AimiC(>=gP053?+S-D)&V9_?!KTv~^lfe?(VQ>!Rj8j;cZ%^t1Azfly8r ze+l&yw`lUIX%!Ij>ADNhz=ePeonA@(<#v59nUXTsg1T2{%o;Wf6_W0)tHnssUWXx zG;4+Xs>Cy#z4H2CbxT4~So{-CgI!^)jc{Z`2$VOHD4P? zK2vAx!74(A^4AScDcZ}L;|P2OI6}hPJH9UgW3I!5Ebf3~*Y@8EPVU2WzmKP60!mL1 z@5~HBB)Bhy=maR_(MUn`LW*Y7T31RdNNAA`8lGBB41cVj?Tsp)UX&X;_R6tTF7Y0o zvEFlngaTD0AUmq8 zKw_Tt5s-CN2_{%0juQ$HkvjlcTwxw$Xwey6S`}+$Wys0AQD;~72A_0q1utQcN~PuJ zuRV$ta4evxD5C)I$9Z9ndLt?HHW7}c3!|!Fsy?wP3R|z=Ho~e(j#hlKzS1z7cW|uC zSY^%rcEebyzUEVhiZ!Q~4dazDn&rMK>t{zBChD3sE3Z_ndwgwp*L#(u8GDbz5d&XrJ5_wp0(m zAJhlSu&WFKcQ4i7b=#(dsOo1QvicZ?5PZ=TV|Qy z$De7^kLe}$4a>N+%R3d&6wUU|6cHWB(=QHlTR$T=7%9OkLYG;#eu&fdj`VVqcW-Vy0~`TUsoLS+R-`n*(?a@juKoOCHXJ)s542m!TG_D91FCd(>yjo+NhtBux@>w+VDDoTD4 zMJscgZ{5IEIr-$2H1ynk-kf%3`mzW^;QC^;_=2n2?UXNl zv!$%*-D(Y7&!<0k^q*j*`E;M9_3;%kmVy z;gOlL`im3m?1oZe*_O$}2G=?)RZMtu`@7$gl$9=~XGQJIh@IT5XjN!-FKA;q%m`Sz zdp0oo>!7ESn+3#si)1w=DSVVoFYuX1O-)cT`?Z!wG7Xu`cKWg7z3k5Xvj(`h%F=_~!0wxEdj`LJTtdhx z(f_fi`-{D9gHM09@YV#XAl{oAul-qob>jnAbQN=vvJsrAF%@^IMDp}ma~GRFbOeJi z{^W|vb2Bd9hseT}7T$yBAUkCffk}I+fHc`jjqDAc_IWcsB^^p1eYcM;QMt{rap_=W zDDi{fLuhug_wrf0{Lha&4pj~%+LC=BGjxL7ogx9hz(V!=Majw+Lx>)~A3>(&%symt z<0Oo7xN@Miu|E#SG$dwLz1UUzZE8jCbE|ye5mO$nT-&e2d5J&T-ta3VOs_7+~q}*&d3*cP*4#0KnM7-KvgK z@a2)LwCFG463=EhdR@#0LpW3asAGJ=)Kn-Ap|=Cc$_s#3SA?Y|Mg+}DJov`f>=p#SG3l(yu~Ow%*%oR^x4jyV=JzUO>-%r`h+ONTXpY@tbImRl5PI9G`D9+9 zU9C&F@8w+9LqW@^JiBx?%|rVBNnE-K0HO|Cl8seQj+JvPjCVwAZD)$m@Xus(Uo_H$FZj{1tW5_wsM3-d8S6hkY!Q5Ki8f`9(kbbS`dy<7zNwPdOiCQW+f!_n%e++Ihr* z7t2RWk8ZA&i4X`_5pg9?jAxXX%0e!mP>J4r_3!9||-nK&ukj_1Ouw3)lKxO%joMYK+Y>5)I zF~bXI$Z;wG0*TBZ!8;b1seTM=B$c2oYsi7!hnYOwOgXPSI+9+x6M6K;QqXk$TO%U@s>0ye4z2#_>Tj5NaM(3{V$mR8QNz6#uUT4L(*HEQzY<00U;9i{?O~P?gjoTN?etZ zAc#(#$BBJB>@_|AUamo)pn!5-Rz|6uuA_3~!}pft8_oKkx36Rpzfr1W;HLC$4rM!D zwi$w?X#z+`h{O(wh>dipt4m!u*lv))ut_H9O9PnJnqhG7Q0!( z(1;kz2n~{E38YbNj#b6U2DQ;xFTjp8XHeuYu3ZeM-mFw5#WHZ7G4dD|g$`v-Sf3|G z&%C-?@$RY~{Gx+kDEt4>)(65_40SdNt~+L(Wq*9L@ZHTn-$B4W$PR#WGJ$ssK0h9| z$#}wi*HzEBkbHp~^=fS8cx(^wUsA*<8P!8&V=KTP?TRW~xjBr89ubUILc%zz*c|3r ztI@F@7O@rAVn4#euHI!Ik4K4TKwxTsxjF#LfQU1d1R9$d`oitHo4>!`1duRiBrK+f z^c~J$PGk4LCLAq`o)L`8ogmZ_I06}UC;<$+4gqwU2-3l@H(oPk&Z|*ytn3KN&^?l4 z87XEz-i5}N@Q$w=&q3Lz#Fs@MONg~wL6l|vBSKJ#{}v$%P(3JMSdC;XnB;_|lw_xf z5)drgB>mQ;^WF(Zdm^V?QY+v2*3gw%N7(dn5Em4`3l5@BV`Cw5HqgQgUMvnT(U4+Xb{&O)^40J4?!6%KSgfz-X4#{1;%m!7-F zr-IEv2%jgJdZ)6)rzllr>47s8`G@Q9*p|vy-D|KzZuH!XzHs~I_1ni8*J1ge0GTOXXn|7? zvtNHAtM60NU}E$NQ&*sY#n2*RG;n?f3Pb}Od+z1^$eF(mgIeZ6wvj`?J;-vNf9AuD zA9+W!v)?EDOK*^1>J7iHqpIyVAJKDNS?GyU5r%9_s1WWSxzZvJ7f<9tekJI`J5>Dhh(t%dYgJBJzwcs(7$R0%l zj4aFAu9sbWT53l{=zZ}>zyGv%y8mbuWL6|us3s(8!Z7q_n!`>`Ncm>xn^ zvFBe8;TEEmGXh-^msAR|tC+AvAPyrV)C!Lm<}pPL{CmoifWO{>RpQ?)KJo%nuDG{w zq5^iY;zCk!ItF2Jm`l7MS&R6zC*%3gwc=xue_aFl3mB$ruo!@()=6{rlCwEN`$d{@Mkmf9(Q1svHGy;)=IepL0AbO;=;iLBHT)?s#n@`E+O!R-Mw9@T)VS9(*Epwv(@-SY7U z9GC%#;PNy;PJ+*xE2kSaryqS42JRkiff1k7o(B2>m=as#@Aa0CqRoqJR5uzZB~W$a z#>4*pX14OiK2Zy1C?EAtDF3EN{f9R#ZEso^-oO^$z)qI8KNjV+SE?fne-|*Od>-17~LqxT0uCo$rST0gK z{-p`LAs|TGO$;S2;9>iThn+g^Fy`u;VzpQ$TbB`Ypn0I-J#$PW11eC~1!XX!`KUIq zA)8ngs6?qcY^F6Ls#heV*9ibUxxrn5?zzeSszA4E?M4SPnx7Q(H=57fporQA{c-EQ zr`vz>VXufC%nskE#O;Kc>3<~F;bqk!DEbCw4_&+0_tz1?qh8UV5PUl}s@tsqG^#sT z7c?k>heZYP^m7l`AL?uTJy@VyYcBSpCkQ1Gi0Y!T;Rx-Ye-DiZ_3!8oyHR_UL)fWe z1Gt&~`QP3CDJWJvWW^nIyZ}IC!X?%E$8?A5%DXu#x{;`t(qDRi=++vtcYFvMu@CED zIs!2|w7-r3Asjl7pr8lY^@g3Q-bN0*by5RdpYi;N8lf_jhngRbbQ4f8j<+ywTE#;k z9Wz+_@GVz#{}B(ETNR+hEp&8tOi*RWR;8BM0eqv5{*?mosMi2IM|X^)qW4wM*pXnE zQt+@MaoA>dY_4$v6Lp8#%1=K(?wG-5h`9puX1~NV_QWU5X5X1Ny>&)S%2k23DLj6y ztbDl>{MMs}4@VKzZK2jE)q(0A+FPGL@0>6b=D{!z8qA5_bBH+kD0ebZZ?r_b!$FTV zQx8>?F%BUAB~5#vT~xQeFvwA;*+l!gR{@+h%`XlOnLMrN|2zReF zsDE~b3IF$s!?j0hVTORVU9+x(D-zV9B#IJ6$B+vHXX=HprUR z)TIi{^?C3PSc8VFQG3>){b*j*jILjU%!TOb+QC_*4LvuN{%w1qXdBsJ#na;Pku&B4 zHTYw`m_#GLbkcK)D-9*C z_mQ1@?(5*Zp*@>SFsi-)1{qp#-d;R?u&AcbddZWE`J8&}($dwmkJ@RI{GO-^6>d+? zMVtg5d%=6y2b5utM!tveQ0PWUN3(AmW_d z(Kat0Y|o>cS?UO=OI0jPiP9K9=S>B(7p`G|1qUiv^xzY?8kE-zf)%doQP-Pj5Ql9z z9{|(` zcz{y0gEJ>Z#DNvQ{M9+mPcvy>(we_u)c`gL9*1+EmM?v3*4tUhLt$vZ=Yx+>Z`fA} zxVPH+7slY0k;EK^uyq+dD|3DueXvz;0|U=}V$a`_V)pgbwvIElc$j&8>VRm~s_548 zti(4_uWxb~L?4bv>M~Z^cIV#bZ&%Y$OL!1<4aH##t3z*L3Z|RTUoU5DgVkYGR4`t0 zt5**OA6etuhF2Y7UX8#SjIV~0+p3pgi>q)ATTp`vicJ#CvIUpI?{RRm2?CvtbOaLp z&AH{rgM6?e4y?F6HS_zMrv8rC@HZz33%hLyZ^)i#+BVFaEpo1lV}57r2ue9)=EU=D zLz{hIc>m|6EvPr!m)Y+zs#^xCs9SR=0{{kBXG79fAI|*{W&Fs$3>M0$I7Ix}!m$1+ zxAjLl9TlGeskr&e5&gBx7WE)w#`^N!GGo4%YqyE(cZ(|Ho8zopgkNYJ_S1| zERAj$dxA-DE&1PJN#M3Fwg6Qxm?jBX<09f{AI&q+EGe?Ii?Vl}X{_=ujYaeVofRWOs9wOjU@9vTprE{&s@|K!j9G?3--TAO6z~HZ+M<+{Z@k7Fo))zQF zwUx1DzJbIE^&`dG!!nr)x~j;~nPbGxjkC2_$0EbT^gcF-a2Id(&9#=VC0#y!3B@?V&zLN%u;sp&}-pB8H=t@#YZo;*3Tf@WZb01 zxFBrt$~dYzt+{aGLtqM7AEPsBfTAh(K>xa0Ncf&(}ORH+e&#@%0~vk=b=W1 z-y*+s6rDXObf#BURhcK&I71gYd^F?wj;+GEKwrV19S@DrSY0DVS|7=VPh_)q{6ge%3pRSd0!_7X0hm=F@rdNaj$iAdQJBk4zXW) zl;`f}tXTYnrR`oz9Eg@9r*n2sQYN z`e!d^AD=%a8|d*Gycp7{^4>kIU+=yzFH+^lr}X#Hf>LfzYZ;2{_qbjd`9Hw@aIzvr z)Lls6xUcDEo*VqP#V-q!CP&^pNbpI|XvMl8t?!v;PaE8JyG%{(5{(54v{nTUgR2N= zN5PqySI1AXorq z&08wa?q%l*1%TZfS7XseHQ_REL%`Ey3`y_q2f4c@!e0Y^RJ$zAC^6RAe zC+`A*{i9~oHf=L?p0^oF=y*&IH^p?HMJg)MPle&fc9TC7R(R#idEJE+zABt}n{280 zS+tdn^wD(Xj1LNCi7lFwg(**pffKG5tdB5UZ;MSuwSyw&xpOAZJp8!JiWMM$P{l`c zyZz4VBwX|(L;OH| z!`JNmGE!Vs1USauvFLqJD?b;QBFX1iZ1@|*M|}$jM1i4)qgRbw*52A;cC$+|x)CoC zB;g>($3LywG<46Tv32+q8xMLZ9|7!Qg&-iMb5AS{M#dhWbSmNSkVjH+DZkGeB6P;j z^F7+`d*1w(O>!T}9G0;WSk+K8T|BgvR908BSKwmp{Y+c5z$CWANTP(k?r?hE<%!0( zCyLgO9lm5pv=du-MZP_CX?>6FF>7V;%2hovWKIW!oRRnP_QBpmJvu9>9+ax6U_9Hv z9V>K4x;APv(9w|Q+N!S!-=|4Jbx~8@X3not#mTJ7mhT2Q)oZkNHViTSXKP{A2^bMm z_5i~Bu}4|Y)%T%+jQ8&*vZSy4P%;+_edD$rInJ4yP-1S@N}!*?y*!Zm?4Q=}8KQyX zlei9-d-%(Jb=jqMgi;yi^Z@uXNMcb!szz+0J$0DoXZ<#kb76bHqARS@{>eJ^!x}fs z^IAl(hp);40g`@<-h3?LNaEH5FTUpj4RDk<8*3C@L2Q!qXsLSY(P|H}EQsA?DWsR0 zK~5(SJB;N*Gp!%`j6DU~rWk<~DSyECOILlDL+K07W7|xfp`hC>Yn*M&?$u#F;{n+t zahb^*GrUE05-pw5NI=T>aQF4Twrq6{a#F_W(6hrfk+McnM4}JgXdqVWN};tb(BXd> z!j*BQ=)5w!1zQm}f*xxgGY?Jwe0CJiLg0-735Z2Wb3e2&QMc4v)wp-P=c$&4gjoQ{ z$l~?L)tF<&lDTv-z6F_`*{jcAhk7bR6L^c|!lW&ugN%yq!=D~GDt}eQ$c-I4@hYvR zV$*KZRUX88S0zC;`p;(5E=;PLM&*c$zT_k7He0H{_rliNr+9go|4ueg8*IHUV(c2r zi;9EmRdc!~96`!Jt}$A#d@&|N0f-u%{906h&0ADwVNJib`cw+2BQXP*qO~@C^$3jq z{$TD`Osrg15DGq4L|1x_h%^?12+d%YxvptfkE#S1ioOw=FMvGw`WP$X_Ml6I@5G3C zr6F#2SLV1KwU4cmAQ}M`-^sW>;OiN7Wq4*Db#K4=npK;sF1qV#?N!(Q{N< zYm}&1eCLU#cLMuW<`F~|Au3vQs^=F+`H?GEx_{3c7t)+hP4(s8F=DY-i+umxW0wW+ z-4tPOJ+221d;R_XC&_s#$6g)>&$ixH5hCi7XkOTvInIQND)ORuet0ItcrOh>dZzaR z7ULH)`g_u7R)PVFnt>)Yxm4C$gLUvw21a!UTjpxp2depabT{0Dx<3%FIm%!ZfriDO_bJcKi&hK zHcntLH47D-Q6^KfNO(oU!LCn|1tW-x#YReGL!mpZ^!_+4cIj{&VO6Pp-MC%a%2+dq zi5VJ^x3x})$-C|#SrUkP|DrT~p7($%5GSSFel*C$h1_|3 z8;Sy0nBIkZO$oTiES(;E2H9mALBt(1w^?l0*EO??GUP!&vtT7CCYWA`GBryvjq2#; z?g2eEMEdoB*jLRg+6ZE}W%j$CTp2S?&@~Ic3bL^QBkqnLZHkj0uwhm%$!A_n-a(Jfk`6gJ~A9fB>P!}2K8K>G1mXxVSA^?9&?H( z6tJy=q<3`RVDF6*J5?e^)KrK!Q9}A;5VN4t!f1E~fy7hROKv>%P}ki1Dp3S56+EWs zwm=cX5--%47msz9IQGd1aLSH>d@W zk!1=~^Lp>2L}q^FlFDN3wLzORgCE?Xa~lIKS1tIF+9VQ*tqO9>abPK^Un^kPEC8x% z2XWm^I66sOcxZF>dYrg+Y&s4WRYv5kC!e0QnccA2+8CfOn!J!z2*<=KSPXZ%+wKLC zwF>OS%K#oE3y)CTWQXk(n&90-OGg5z4B~0E5kTB-bHn<>?m(PlkIgnLQ<|W{>=kF& zEeF|t4}yxLp;}my>Mjyg2I7L<5kUbNm^hUTkc759FTWlCIs2ipJ|1ij#SzNQ_vD_5 zt(Z7bO9;d|KB7efX&f3gtS18rgQ4{U4CzgdP3=w+HQtmCI(e5U&rjQIB!f4H1ZFLH z2o7%2gs^S$UWYv^hXb3+sD>$+ciSKx54Z)5exR8mfJA5l=ybv8$=XpJ4jM4)uyu@P zN+WPql!PV#CN$#G?=$KZcH;CA7pF0OHPR8ao>=C{LYd>KR5k<(d=f}>7lH}*IG*lt zv<`l&)9LVeakP8}%0gEMCxE!@oc4moS(iq>r;KYHf*NfTz!7!n%*_Q%qsLOCh8idX z2UEk6eU^yfdd@c16X$~MKZsG#;!iylNWx_k-^c8O2c1osuhYK~tkJ{i|12+r!kEho zAbunZvL{aZu(OY*GeZJ*=A@qex!KXd8EZlGyG$R7XhN)+oux?yO+D2KyJ@e%k0mZ4 zH~>cy(*?1cr06C5bwQAcR#DGN162YFfJMO6b=PY@7)=R4ENJz zGmLP4*MVR+5CxRAp{}qE)}c9@278Vkf{s~xj5iUiu;g}DmXk>MhYUw$WSkc)(Ul4@ zMaMzZ$UC#MH6Cu)Jm!)sMw6SM5;W2oaYes+@^QMe#*)1e-t%6nC#IgjXd)O2k=3*b z$2eI23MScemjC!A9k9D{%x*S%4x;TS3JUgoO7R?n z;RGb;h7o{V8Jw-&WPz-%Iqkv-66L-w{ zBdh;MEi*5f&$yFeAoFjU;0%P6!fea8kAMJ9kaJOAh@|Wc6G^~LeF2GPr&~5M=N~}p zx(R2g2*GWTWZB|reLzZUZZ_B~*0Pg0c1KnzRl`S9=+R?m+S`Zah6pmzBuO zAEAEbf`i!-$oZV}X9$6F6@e#NVIc}Q^~%4x3NWQo|92K5;Z!A(aJZhFR2SgYwR%l! zPU^EqnzI{pTi+QCl1}ia!f=@Sg*$uavo2kbkGR044Ysx;p2Xq)C9P=hYBE_k8~}3r zlMreLGDZQfJm%K%Ai->?W=0@L=;EWg#Wyi4Qf6KsYN0$Ca5;9jTM}eO z?nOZXoRjC0J~DuCX@k+32BGJH`S5@+CHbGl2*}W(!yMRH*-5g5Rst{y2;0Su$Zfu^ zT(>j}-l?Vgd{{LFTzJV~=>W1+!CKqrmG*-bLO7}7!`dADv$i(rhBk16PN^ti`M29b z2ypxNfCMOck(E=82tk6@B{tWOY!>>jgfCy(GFwY+28-wKj~9_Zq_Y# z#B5ch#Ue7cw6_6)^WZC02<&o_iXB1Q4!HWH#Jvn5;zCf1V*}%0P&^2P1i{ba*Ynoj z9trlUTRkQR9}`$^l=C@~{n&Dba(Wdajtt>yS(_QU*m)_4&m~0QJHTAeQQU_xR3auO zsYFp6^D&;e#1K@*La;{?(Kv!Apx%O`gqDGQ>>$!yyYTKy$nV6Y=2bb{U@NUT(Tppz zA0eQ!iz>sxfkO*CTfu7gE^}c+G*qvknRbab(P$e6pWqk70rFKuK_K?F+dWqxRAafNTN6Ls(<$}#14=sKt5uy5oi-c)DP(rxFVqX z)#f``+5&8hjR$CyGX@F&<-+t_{`TK4jO{vyXBB{=gUlzdSqsH^GQu!7@o)8ajaAo7 zx&wDS$S?CO!5AQh;Av}jfx)v9%xnII?{Tq7VbMb#^+lM%Ho@fsFnP!Il_!hdX3pvPG1OaD`l`I_*#J|d2zjO^4yFxZEb zm`9oiL>k6N%BJIuk4Jp2gYpIxe=>>ZKszc|?pCRW%r!^QC9aH_->7xDfz~Ej5n#;I z(Zh^7cN#^74n46S%Z`g;bh`lE*Pzy`*Sv22TrY(p9|y8rJ8xVDkIfyz+Cf9=UFEYQ ztQed7`q5(szsBbx(5naN`gqy>SciHru%h9yN>N!&(PR!M<9^Rx`)vRm&>7ccxFU#x zdyAKMAPFFD=F}Y+!J{0ojc)tL_9$bEmB;1Do&2)62#l|tG1H^=q`w$!m=Zdp3<~DKcH%SNfxUtOA;&vltr;oc&2xVs!?RVyj zo^tUiBpm8<`l>Wp<@01UG{lc)nk=O1KenpQX^|n*l&zezb1CDBt;QksN2Pps07qns z%0n9Axsq}e!%xAk;QVmDAw@vtDa(^m@B7C1xWk%<$7$Ha+3j=t$JW~FUgQRj?R|4g z4>e9Jzp;3v4~@xNjSf}q<%B(Z8sKdT6P_RQ%CuMf-b$A8sSFmhcIiC1T7?cMQ3r;8}GZSe7iKdYc4oA2vFAS2N>TY8{_E)(IvhXAi?Gri5z zXke+$PzsHI5eHVO0s60w8^Za*?~0f3MM&ww?ut0r-{qGtRj-#$2G8!aaYi>%M%+R= zHSYz7KOw)pxia%{T8|&uYrCz97>bApNEH;&&=lzi2nb^6NKx#d zA{)QIyL<1>Zt`y?bCS$FlXK4V`n*r}Kr7GqbtY?g4~79!Qu)mT@EDE#9K}xgJ^|qo z!gcc#b-Q0=pb+eYr4r@VsW8+- zuVk`F+5|p_&xUiNSC|P$)aSJ>1hr8iJV6G9js4q2kvC^*@wa_~CBrg<7>TA_>i%77 z0WUhNsgPu<_j^}yP{2v_YcOwBg+b$P^Y95N5&BectcVq^XP1(K)-#yUfM~8=H-j2- z_E0&`;n1K>D`jltv>iv6Y?^Z)<3?n@b(C#1_QsJdvM`9mve0&yPF@QBP}o{pRpZs& zC&3tOMdESk9;>Oeun~J|c5)2P%;bFsTGR0^;>;gMcY}@F(H~W|&Xv?|Z^7HMUc71k zm4DiLp4WRm`q4wSUddE(U(LT4V4=tQ?5qMx&6mxp*_X?6VqW}SUx4CO`pvMHQ3qTnG%hFb&RP^f_hT4qGVe6ypthja&ZFM${ED-epPgQ% zG@x9}a9yO9kj`_%TE33jCobD~{me4jxhobdaHYR*#SP^`hOB_^Mr&;GS|`O4Dt;7g zbu*=%AlmOxQYX?+-}STBH15f<(W7Lt`;nu(yTl#u^e~9TBwdwa$fv}7t?YhR>3q3G z>?zQF$w25a>MrHCilMQ$eXFk<9>1}!an)UnMw3R{QxG;>;*1!?&!N~0q zPl(z+6dQbCO4t}M4pX5GVIqy!*Tb%5hI>OFNe%C*UJq~o=BjAu(8x_3wR7MUVR!{q z`&je}Ex6xz@~aca1x`^k*dpT9nu!T^zlA=0>PCfom+%lFgtri zZuHAk#Yg2VSKY0ycbvOaF>|NGVpDO}$*?}GAaGkV0PfY8>ayF`$}B=s*ez#}`nHVx z%iyhhKT^7(!lrz&?t{>wiG8V6L8kVR`vEDSEz*a%0lQmFI2lTc%NM&{ov5gn*F<7( z#j;pw^q%_C%O5Bd^?2Sw;==7s$9~<=b`EIO55L2OY|p4dJwe$ttDloQexi_je(Vun zhtaMo6vU~HbbNf3-7OK$TiNRyvf-NcUnRpTTI*^yFY^t2W^ONaKlv_fmXwB;2MY&G zoyg$2fztX$0vTu^j-!~j-Gn8lA6|`gmpd<$BDSrd^LuD?XS`+AX zTE`%x2vq@10K<_FNcVOZWZGhhX{UdbpcNiELHu=H%9J~T?ll@^^FRDBNai4Pq=sLu z^GTin3*!cjC0W+_i<%uvl-;n751Gb7%N*ikDRl~B0&!!PedOpn1i3r>wLeSiVaW5I#JS``TqyeP=e3aefiQ z8Eo%OWos86R9F}5ttKytZCVyMI=Q#oSu>on%+EvxuBNHB^5_9)I?DM(hyZ&=Xo{gk z=+Jp>*WmhNi#LwolhbWXt+@{*ap&r%;8z?YMFQCNVu$-NyLf;ZnwKoD1wtB_2%&FF z_4mG;p6a@YJ2R4~JG>nez@cNu!iZ;J@+0$%SF69^@6EbIKmGzoeY6%n1-EY)t27+m z^GG0wUn(IJb#}*Fe{1#fD{m5kH;U zE6~h%%Od-OnOTL*BmCnutxpedKIjGW<8&Dkuq1A;6`??zWA>z31xNARK)>8=3 z?#t3W0!#!wC?hq^?hc-@5`Z8gmba-b?x(f}2f!MQAm;pDn0O;pUXPX`itIspqtns! zgo=187O{fNV$KKy>wERlJHHSpJ3ia7>dE&v4rk^3bhDi?M9_08nOkjwWE`~2NXUE1 z#YiT>6UqfZ9WaG`r=y*zD~>m^}sNj)nVB;K5i#6a}H*23YaufdXkYG zvaW-T<$dXBF|#`+O%(PCP)!KTWW*Rc9o0;bJXuWm}8t95kJS-2}S^{QzBOf+4^u38#cfK?H*3;!ljA{eq z1N~oJlVjUZXM4ClxNc(Li&znE+2ua7u>0s7x7Tb#?5mAi)ju@dpUSH%Z#-$oPlWi6 z_NE5z6Q`>gHr?wx+IC!m zHpVK=+pPpUAN~G0KmVF5JNlp&T4k6WOD;_>i%YGa6$;$fMwBSLo-`_r>^!g$xxCxB znhkn9-8*#Utl-yKQcGS;89ABj{!-kCV1aq^BW(>AzShaKcahxgMIxGTFF!!JOS+FS z2Fq@Umup}eEXaiNnYhOFb4hv!r*$L6BG5(}{86q;_bb!1`LNYR&(`$J{dl-srQP_YCsS8w#^m%p7L?X1?fF4sB4j(7tbn^t)YjQV)+EGs$4P^Y&!HGTrC=u zePdV{PC5OpfCp8t%P3 zcpXr7r}yxo=OxDGg3V+~Qhe#~+DFXSCWi%!urZgA+Nr1% zWURv{^jQb-^iti4*pKFTus9BNsNfYEP=5YsrB~Pg`c)eXS;CL4Dw&1lv2$G+%pFOo zr~WK5J29%bck2D5*0-HVLOJ43Rk58wv8?$%n4%mVqop2i-`b0uZJ8O&e<1{{umr2h z5;uk{6*eis>>-*ViI!tsA* z@j;4Db7>Y1bk8(~?3t5zIQCOv^yTX@=7QYGj2z_(>o`w-{=oo=UUi=UvyKUvc7%1h zYeX~4@1|7M7$vh0wo)FE!(vR6zREfp``6TVJz2sJQk^gN0NUS*)kaLti0GWWAv9e0 zEGMnIicJc4c^~3B2vO;c48ji);8IfnYoTRRLeGxC8`-oj1;$GSt*zcC4uFm_a$2Ep z#M+5_n@!D@H;U$Due(lo-V=^eQ}@Lt7uGH*4Tkz(_A)+^P(OfyeLhGXU;6R*CxsF zs9G;@Xu|!^3eO0}-#2%eu_eE)&;zxNzHrMr`soM0Vzxk2XAdU&1P&ghoTAUUh#toc zLS8E8re62<%p+%@&`@^RiD$vY-3MJi*(r_7d^^}VEsk$(91v>BP)0Pfrn@a}TXz1M zIqyj1O~|+#lIO+U@>s1j*UfiXa)HU~n5x^L6lN$`q-*C8yhkSY%W81Fo#`!p10HMx z)NL1TH*dAnJ(}^oF!SIIvkgjN^jD@_0O+btTvTGu_mxnDrsHbykB-4xz-g^j*TiD3 z(oZwQ$UUig5vM8vf`$Qu9uX1-?cums1e}t_=Q6bRaOk4bne_-s`uK7IZVyABqXVa( znd;@P0}sM(mMr=@QUuna=J9t?Zpx1zH%aH#N0NQcXVQqA+R_lT;(oHg0Tt%hk;EU@ zlUkbg0gQc03s7xzl~9Pgw&Ss;vpTsR zI^f`}+@V4L2y;QDw7%i|!0_h@qq``SNfE-b%E|JPi*+ql* zMZ4TN_lV~4ORhU~m-|8&C`#mN=8PYaP-}9sadlJG^g=OaB!joFXBVzj2p!r}xrOME z#nGv;ZMwNYfwaUz=ez;{qODEOKhfst%7quLfx|gglqa5}yBWG{2VoG@bU#@Am}bo8 zNzEOf?%{P%{FKH#yqvy@eWyVKSc218o`kVp#8w$D9kH~B~ z2TkE+M?tX88~i>Rg~AufCGpyu0yL^}Q~+1>Z-K4;H1&k>J3dju;(+2{3LkZ!16L!$ zp;*+R=h~BfNt|zXf!=qP{ zTUWUE2EP>WF71b(S=~ut?c9!^jJ+?wa3br>@9}qu)}3AWcdPQ>IlkHZt_t`ZK_XEGGY2|WvS`S&Z@jSK z7S&qW-SBlo{rNiK_FtdcGrx2A`JY`HNYe14MBjtr;VUEV&?ZzdG3nej|G^Y|sv^qCQ8bM);+-)%*ub zX#8|o!kCGMQV|Ln*U&P#XaZ7(0W=QyoSB5)4a?P$B726X35I=&d~9 zQ??dKZO1qqNtPj5KwXP_V4kl`&L&R^4726{sae0gD7jRoO@ z$>3$6hj2BU4oHAP`d(>x-ZiOLg~WLP9U7zA!>E7<$?e}x`oK_`hc>Xuq~}%CsM%vl zFf~#>y?4QY%a=V>Qs9JS4lO|eS#n`8F`$?tQ)C7NGI^vTMF3TrDjkzBSfiZs3R_5m4@-Ox5iA*J#SFoH$CQ#dIw&EeA(v5uHHB@lw_nc#-1ssn zGLVQPK7UZU$iP(6h!%^4Ywo9$zcOhOQ&KyCJiq+-*xQf&OJ09~Px7XylbHh1^t(?i z2Zy&8Wr~c^Y3NDRA8I+d1@;G*gs?`rIps4jmVTNCg6Tr%DU*gZGK^5p6?Zr3!)ds!h;D#E0f!%1d~{O zvskMGYJ(@P;K)o|6E#=gRyPJzo-?CM0Tf~qq)Dme<^(j+2?%Qh8dvRmy6%2lEej10 zKnE!*0UWg2M~n}Hhe~bU-h`2vPVFb&K8?oEcMnUU8L&0RW_1H`b!*%3_lN9td7w2~ z-R^eH#5Vo+fjp*P`y~ML&S)3_Mv_rIaaBG6HI)H%2l$4yq5CK3?dP`*yNvZJS$Eec z#ZWdNJN6NDx#($J>U-H*W_)cGU*oD-Bh&@Ce!xC}14o56Gw#&w{A!RRLD@-4bXdaa zf)=5He;>;-rVcfMQO#QMbvz~PQ2Nl9xMt~+X4!>mDgd$gkChavw?OEugjW3A19U2| z?&ax5?HxdLqD^$4>B}oN0b;X(%VTS5jnP821p$em2Pl9xSUH^Tx;n9p0|B)zsAjj} zHkJt>a-SWC19@w97zDQFGBSblYM?zVrugVk&41~MeFt>IK9gffyL)_{duO}0wXCnmAk9m9OC&5fnSf*!kPg5Q? zGSO~8t)Z6fPa$+tLZb6BWbAO8n|bH79JoEOGZ@z_s|%shI+uQT_8x0?j4?9JIip2u zyI`No`Aiz)<-iGn-K~M$`|;f|l!C9#a4PDV6n|F_|1*PODB%3hlx&&*GbQxQk#pyh zsRin{-&DB z^ds($+J_$bpqU7)cnkD*DuBF>vZVAHTRb;Sc!5Ow$6Djaa!3JqQ|6=|UoUirGX)Rdq)>iK9?9xGyhUWBDFk z6$yk6f2bSo`~%wl1KJem5w}L&CJx<`AK#ZB$+19UPoo3&!G{S2Q?8>h&;%!TLj5y1 zB%Ur0hBxmFE7ZeS_9k%5Q>n+4I+5!@8GiEg#i3x=Nfp;23;Gm`$qr#lkjT4nF~JE9 zwkOCY)aswt+gek?=qV-UDPbB=?8Gh%AT{63a4L*^$(iJ!0`-Vq^G@_H3ywd#<7cc< z60Q{o6R+*Gr|g+w=Pu5kE1gB+o`_pRF4AU5M>AGh;53p_vpk!>HbjU_v)vi;|l0M^0Wu<{`nGmq{%jMcG+?X%d&!V zVsFo5`1o$V^a#HFZmG<2h*CeLoA}R{{Jb~~^MI-I!9A!8Nlr8f8x4=9g*1-dg)d8# z(qKI_&fw)g7hl7a=&xA=E3z>IP^1TdqYwRfwoae{YI|(!p-`!@C9j5+J+OLy{mXif$5*@@T@3*i&H*U-(1u@$1?Pz9$CjXwyaliaO8%I-%Gj`mDqFS3 zB?VJgIfuw_~Uy*kyo+xZ#t#B4@R+`)*qq zKp{Y|GdQ*fqvP!E=*PIN@L{xu&?dMCd}O(HHFp}?v-R}v`&xw}dduZ^Jq_ISf$1~k zejvID3$74aAx`WOXnW54!`Lubo%_f2+?9s0Z3L}{J`?j4X{Xl;ruBCh+Qi`Nzs_>~ zt+>M8%V%4nrMt&}*Fa}s{W`Qsfln;gA?b9BNkA_C-Fv^Z$A0EBJ1guNf&DIk+<5l6 zu53ka2@VZH;~77x>7bKLKR_l?(~1j|^{Z%ibdvw>(Q(-t9d`s%RK+dLJ$Z{Jix1+r~$R7-uxpZQADAzT}kxowBbLIk)@xWAqNQ|Uzy7e{xqzKTYk!W{u@<(pI`Cw zox6wgcmBM4_UF}!X})v7y)#GCXO5IFqZ9u8f-J+&9bYMkdiI_?_O-X+chmddIpe?G zub`>tzjmBqi^@SKkp7gMIWD<7`u)?NSD)Y+i}WQFt!cz0{!jEIS%3*1b9q_!-B~28 zB20QJ46->F35$76ZvN-0>9KS2ucxGG)%6&Syvw<+`CB`QuB3GLX-# zn+k_1PuEz}+pmh=vPw=uDc1KSoA6De4b`0CC+~Q&aT_{v-&fi7Q(76!W=U_&dG)y5 zqA29u>pL#?s|Io|EhiUeHZ~e6$?C4W390+b%q%&3-ddmS*&keI{@7?3)?4?2`Fk@` zhONqwoB5(Q)AFgDB{cW@J5`?M8x~I+*N1Wi3zv_b8AH@?t={XcOWiG!i>-Fwq?cFI znGuGK8sQtRx5dv2_l)xxUNXB2K~y|7_MQ!qPrE*+9es^b^3l%DK&GKyGqyei5o7@#y@Nm>XT$jma|H^wGI*+E|7UO9zWKL4t# zYWYDoOeJr*vJc4^`-vyHG)^fh3tl|D*pIWwA7tcGPV#PTb&LqhU=Hva9R0l;f6hua zjto=B>eX1htcHt!#EU?!7PAFKtE-M}%%u6Ro*Xi=c@kVP4CXYGQja4^FgeB% z>t9+DZ&##ST6E=Cxa%>CLAhmjBx+jt;v$nBd5hcZojknjxyQgT!PzgeR#O}OEwhZIow^b=yA<3`W>t1hk_19 zzDZqo!HqTt_k^9cX!qFEnq<+8C9W7+baaUy=fyYvkGd5Uezyl$`NuZqcyXei^vtpC~>QwY$YQ0+}^H51e^1@hXc_U zuHB}RMZ+5KvHsO%;OjJUV7t>!k^VX=5Y-cgfwGMYMYqxd;!JY zr-!KTCe3VjTIhpT$b|0w)vsL$3WThnV3Y>Ja|)M)HZsU)?dqJS&}%v3vxz@96Qq|V z1qi%}F)P~Y&%$5t>68Nb41>D6eSHOPzdk&SUO{KEydp<`?3|TB?8?Zdu*43kKTlYh zG8Q_77f6yYy$=huFCXp{cC%U)71Ej(qA zmpNMRLKVu-r01`KKmIz8h~zoWM&&1q4o>JH);#n$Bnhc<;_t;q9;*ns5|h~TOQE1J zeSwWoW=sSU{dQBE+cDQjuJ2BreYJ2Jv-WpH_{TDW9%ovNiJKvpUloOJ4e+X9r}HjH))=YELZ=JB zXH9IY3Ag-&;gg7tr(6v}jqeQThQz_s?vW8A-H?Rc)Jw2yOqQs1d=;1GTeHV#T=c=& zcl%BI3%ldyP{CgH;EIuMAA7y-E(SR@3RxB%)u_)U?{D}P6r@(qV;UptCH4EQ$TyM4 z`Zzs9iHcLB8;_N)$>nL)?k`NF*|%0iRuf_5+ts7JaMoKpfl~Y~^DMegn zx9mI3U%_5Uzs~X{ad)h8eGDCtb;Q-Q`UlLVhnH%)9K`883#01U@b;48f*hW6JV-y$ zz75FbMR%>hE|Sw1I@-EvppW-m5X2~+M zaXTW0hRDhn<8Y>Q7`5dwu-Wu;LvXs|iHdu1oBZ$JT#hK#AGz>KL0=%@V@nUm*q4HM zDr6-FYZ&Sh&iWbAK+oVq{a{ckCv<7&eEZLCUhQwjf#fwy^L{pi`u*@nTH9fTojh8^LoHgZ2v!=u zbmTy~!109N5!ppMhl}Z5h~G^=R{ErYg{G`|p%eL8d zXc2JuO6usd_3`(zYl4i~4lX>oK=hNlB|mOG@7}wHhujc@^J31tx(@Q3U;xKJnW&`i zF3&kNo=tIRZInEI{zXU!4c=LJzSq+A_iK{aK3=V{A+wQGC4wJdPk+PT1Ji=a6R9wR zXVh?d&gI#GGMNQ0b^HNz)T#5frR&KKl~oHY!r9AVl2(#YQgTu^e-c*zlmT1=R@6w* zr;=oXdf;AXzAKZF-=1*RoIdVJXB^b|+u1b?=@rq^52MzU>}yr zbm<}FwH3adiv4}p%Mrr-t>@(Kb7cVo5l+&s-Yxw={cK*9(LkiD3Fwp-R53$O#uX0_ zqGA&8k8Y;t^a5C{{(@W|{@WQnEqyGI6ysQ3qLD0!KW(^JKyPp;Ph{GYz+42fX@1|S zy8hM9ez|`Agl!5yLlAcHy7Go1&?GSpL+oI>kr$F31OF92@B*X3?wAFm0$eyquu8IN zzFugnfm~}JVlgRl2WzO2#N5@5HF)9eWfY@DUJm?6I|3F9_|T+#R2XxI;gzld*O394 zJH6(0`j|!4tG{3(7(<(gm(eN4J{Dlxk`l=-V}{I<<2(E!2*>ougO?W9GAW!s_}dZYI`J>kQQH5ijvnctB;N~7V#n-o{Uk+``chFqKV~RBXXsr1kd#KcR1LsIc$7)^8`7$eysA$E%B6IP;D|s2pcY$l z>mOrf%#g_%STq7zJua$Xg!nLkSaUnXf}{zo`^U9GMW?^4VNC3iUEt6^kmZI266Pwo&V$pkr728 zpPjQ>P%&T3A9=NC^mq-dOz7%vCJAEtS4M^(Xbr5%kH%+=$JYSD#I$<{DcsnR-ZlD4 zKX#9Zke5!iIe^kHb z>4A?1c?&w*m~&->Xe47CCqeYs36cE5V4@{uBsbrJ^v$Svs_s%zQgn)0(H*nNgmJ04 ziGw=Q;WR|~fRKp;iBLP=2+-w*MJeSDSG^7B;^fC~RM3AV|wjEk;r%&nb!K*Odbhua?Ns@1JD$Z&OI`w!$yAe^#YSwUJ!WiY3dni ztCRPJx>LqQ45w5jz@`_cK+>G0p#Xmkd9>~nxCX?Jnarf5vL2-9te=w`uz}3jwAM|+ z_Q2*d3OFRhP{Pu9ouGEr*3=s;IS5jn&b)*sd4w8*|M3h`PFJRLD8MJ3W*|Ky>pi&_ zVf|8zz)i8XSA|@kSG3Oqt>zCsa2;C{c)gx5eQ?J%e8x_4Fh%BoR86mh;8i5(m5>gj zY6?t?0J4&XCo0&T(Vp>BB?KIe4bR(Nl|{e-n+Kfq&~AEas;#QFgWD)HbA2W@BIEd9 zHN-@3eCl5{#Hr2!0nn*iiUDTcM*;5ZZ4FW&e5`u2;059!q%rrA2quos>*k^guT{OL z!f!cd9+8D{;Cmif?)&r)?_AeO*BWHs2&D5w5{3k_!p$2@IOMN8+~Okes1B3tKu6OG zUf8r+l__Hhhf-AsQT%Mg(YyuVEOOvjO$CSLfj&c;|7ne)bJc?|rGpfH66k@d)5G=I z%SY$x6(%s+6K6ZX)Ws9X44`>*{;E4!a5k^rIkg%( z+`YK)SbIs_B#Hhz>kqmqL&!uVSzt!-Kd?d0x|Ly1BXNd;*s-9!nWZ<8?>>3I@!>k( zH}iHcCFw=mg6e^riru+o!KJot*O4Rg;l20243o}Kle_;XI4Th)=LV0RjvL;{% z9E3d-Bxpt4+jD;qNxU)Y(5v8zKT45xT(bR55}YH57LBKm+OaCqPj)@ntIvnw!4iaY zYrv}de-b1`gAZT-mmtx2a}NhUmW?D^PIy4wY4o?7jie=%y1k`~xIz0YF}iC^I_yHQ zd?fx%e2=yQ%Qn2n;gjUJ5n+P}_5+xMMm|Owe7S=>wI_t?09}?&!p!w`Q^|_--RyHI zYEi3DDR zv6S8;@w5kGwAr;X=={mYTGCtHh-h1|`rzJbZMXAfLBe!ZhZ_OnT58)Su_hzr!S3T_ z$en(k1uj8Ik^w*`j@JH{&Y*bQ{C{)?g}(Zx!fL=gMLq#S>o!I-b@<}mkqfrK?Kr>Q z?_52&i2n>r5xDiLC)#TBP+{3U$~P>-R{{+d86^F^$n?MSBsIR||07RQ8^{6AHq^)9Fe~p*-s{1+G|vpOBIzXf50E=%=bH9j*v}<$Y2BR#enZVa-^ibr zvORJDlP4sLXF%XvWKKUoVl>CmkHonJf@dU)6Cm<6`13MQc`TV7yf-;0%jh1B3LvahK*1) z-*EL$pEU}@E9|#=gZ1n2>QD5Me+Tb(cL7O zK@dzD1RaEl{}r~!r$grejvA2p7Ra0$Vevk~>RR}ES@`Mp&>(vvJoue)-u#EwR68_6 zH~qV#a7c9SW|L1C&-rgB#v|RXk&r|Hj)lt~0I}h@AD^UN3Qdj!g7gVd-o8en15e){P>9-)qiAs>hN-w#c3APPy6jgddz zFfBMBLOu(H$p75^Y7~Nb`CVo|#+WrW`UIqIJpCjI#%q#d9Fu0~e7M<24}*bmII<-T zq^|)yZa);;{%H{sE)>0Q)U~I9F4$At$8|31YNIfLTJ~ z0iLW!p(dwsAJa2E`yyVMoC-eE9bwmB@~Gl(SgcWtMkgwaZz{gCDZ5^(eFe^CHZ5%< zVtz|)9bqSH2Y=EOpwQR0Gu!CtaZ7>dI#$edNx++^vwIe4Ojb$7&}U&6?~)*_ia|w= zD+4S1nwOcvR@OK$3bWMRu<$`ktb9>J$nmp8umC6WkU>S-+E_xO#3ptcZpYT)6v-9R zeY9c6LS!)JmVe>YktT4-t8rOaP?nY0^^aAScuWhQGJo8k2bZx9(T)W z!`(-P(uYzZ!tMby;<@s&} zyz;J|xs0jEpm=W1)>Os)uJ?S5>=$RBPlxbr;7}+mk)fWiM@=NNJj13;jaA(*EHb{cB24lN6zY<`kHU2cUmCKJN%SF+m%0mWEQ+H?er1lnx)GIaGqT%S1mgMxI$uyjw`-I!61UhC75&F1C119z-AKM&KcOF9==mUnt*WRaVpiLlf%E!x>W9g* z?_dzvFSj>NR8nD5GV?serH#Ez7tB}s7P{+K`sGE(t1sL&j_8%3aIOLkuXoNI zpHx(x@S$C-VppCY4R(6ZZ{9<>*XeJUeU6EPBTA18qwL*~7{JFGE5uQ0K1gM+!cY}| zNa@{JrU3^-O8{&8t^WBSy=4yJv?!h@tw&=X6{p!?-K+JhVTyL^VJ11>M3NQrFNBC` zd3LtalM)sl=anuH^8Gfo<;aip5b<{k>%4VnZI+2UQ;QoKKPwJQ`-R4dF!m{pW;sHV zY(|n`e#M|3Sv6apQ9$@p!cXe9pLvXQE#-lgUDMcYV_0fvDj&yTgCm}dF`8JKMXL{( zC@GS0`5xT5P!;}y%p?-0cr7xPF`f-VjVsQY!H^(R=Fy#5G!M0{jm7P*ZKSSTczJh{SpTt-nw2?1}Fwl z+0feR^lO6hAt_93p_`h*hNe(w>u9B_>fZhNM#|@wyIp^LV~C9~mNnLL$;3jXm?n@F zQzfA0H?4Uw>R!@%P}$m4#eM1f^?N}re*Jhdt9)3dLWhW#l=k*?!eQ%RxF3tQQSRKy2bYrKF9X6R_Rw0pGFl-Ne1bIPuJs|l(tUlrxdLATeXeYlBcCK{j zzE0=dI2E0Yn3+q0`B=YcVK4CV_cn&Ggn?C=`jD_1FUuz>rtwz6l`ebctX`BP1NJGLsIc zCxnj(Vo(etJxd{aj5){1g&6=qt+WsmQr45-1xvno15~GBVuscPId!#Sh-eA0a(~)(!>z8m?$>u=7w%h?jUyua~gW z_wB7y7D}8l00Tf3=SfZKnpKlqjC-%EHSw2`rV< zKcP4EIiB`&-?KN{pK$mw#AlVoV_`FOHEPoOG+z6OXVfHXsVCUe{h(~p9j^it@99b{ z`7!KeTkBi2+!b=#zFQWZWtJnh7}Xw6Aby8a)=a#Z7ye#T*p;YCRnwkZZs9$N+6*3K zy9z%mDU0B+U0E!@iPR>eHI#b-3ugj9zcpL{;j=&5O`%)Zl?5TiZ_k-249*D|tL!d> zY~hPXfa@mA^CTHg5DdEkjQmuo5a7UGo86PBsT|K3%^fA|Kpy{kwvScYD|+f%L0VWb z5WsF`cZ$>5D&*Nfj%Y|hC;VbPyP(jbcNnUE>J*Ga0JcCYa;YJ> z%fNu#PoEHF{qAFtK}gmi;W^kvu)OedVx7Wfxe=4VB76@3hKSLm&L6bMF5Fa1`rhR# zRx7yHt`56TZLo@d3x8z^wqY;@mTno@v3tGmfVt}qJg0ZVhCwDYB!_&b_!ihi0VB3Lj3hZYs*l!yy&7Rr)r#>J{g$l(i!b2CuXl@pnXQf-@ zJ>)&4XcOj!8+ORqVlw2IsItkyG+1#$YQ~=)ak+jugnzGzatN9haTj)%`l(u9JU(j< z;dLgi&+-U+Onj6Hpf#lKj^C8EN-Rn=Pi~^R0L6bQra7Q&R%Mf_QNK*wVH~#q{3Ed; zF&$S25~%)jR=XIcSjGZcSM22HRN_>9t#V*FoyX=zXibPkSRvmbA2mvoW=JGDlhZuK zT{l+OTQ^eoo;SKe%xo&*kHq!%HR)qeS!7wTRg+b+)z4%1B@x7)Nz*EV&ygpQ36VYn z_&g>&_|4qS24yDAj(q-#9%XwC8P%oo z=?b<2Sv6<%-QrV?_MFG~&m&HT1uP?}dZQ>n8lWvOZkeK}Tn%>b4vDZn$HXXg2%pdRc3Rd)ckfzVX5pM=k+3p_evB-bCJ8{z`sDegk@tZa?~E z>@-bnOkj))+6e8eW~usKyJ%~_5pQ~6+uUC>bD5r0pEOxA1sMg+@D8;6^5_b&!7*2} za-74nJc6`N-DaGa@zJ+BwI;auE)y@)t&r7|>HVmswi(`{>BT(6vW>U_|*XdceyBsj74-p}K6bWDzBr*o=Aq z!$MkXIv?KtJ5B66%5{nkY9Een3Pa5vtsiRNEPiuOL}_+=ChTN7ksla22w%qS93*~E zEKnDP$RBEVpbMpK)Pqc%=MVM${d0s_ng(pfvVb_EU_)!7#0JW8U}iKYAD2YS4>EBv z!f3)I!fdu)b|n)Tvx{0A(<3w6F`u~Yw5u!7C}@0n+!=DjSxyW(t9hM7G#5J#8IK|& zzT%4B@z|ZWrnHt_zt+i;(U)0&8hf9GLQe)=B+@A%i{n_!aSt$aY?U}P#8-m^x z`#r{?IY!UldiuA)PZJQ09 z^BxLz$JjfyUtJ0YO9~&7Z_4La8X8wTQmz{|>{}c!DxTKp@3ii0t{G2`POy$Tg{r&^ z&IRvY{{$u?*^-*_XLuw&5j-`Whs`ET7s@J?`Yt@RTTDXEpJSgU$IK`uNHH8kz%OyP zg9~xb`?LE$nbpCwG0>}u9~0Z`cm-K(!dwsk&Xjz%cs+)mkX!E z>pDM1`2whkt zBU38G7&2x6*ePUZ#H!|I?$_KpH^*8YW(TxoXriK0rOiD<79zDSSD2wxp#1ULWsvY8g@T25LD^1Md2)=Y-R zw%<&c+^p^Xy0!oSzZ>u0qP2;WA-S8im5n2>n*im%F?j!$|5eRQN&asXCrbfJEd^zA zF@DVq0G;*-8bF#3tA^#U&!*90EP6Cva{}S}S z*Z+K{iJQg$NwRVLr(1tJ$o#JsW>zK^=KtmWmz4iswYHjaN~2y*bS z^Zy(F|7-a_iT_2Z{eP6KES&#M`Cl#nQ1UbXs{{Ylq5m1Jf7kvUE{O9zr#M3sjqU0>g zvBOcZ^2Pj8z=A*Fa8IKcVv`U)piyjNkpNA0u`stz*=umIuyD}(?Ku+({~riYfX(gZr10GnLtj_hV0Zv3sA{cH_TQA(R947* zA%^C66?6G2`In@&=F}&hQX&&g ziE;Td_PugT`46xrce@>GfrXYVGD}*jO8DD?i*jxH6-2e44MpIy4ln zvEnpl(&pmSnAR!ptdxcb$uAa3-{UYJOc}C~bBagHL?`FUs9+;0ta4Dn`~&_}6#n{B zf$nV|zT6C4dT6&)ig2oP4tLb37Bj6@2`XpyfEasTZ({GfnxSuUE{2ENo*m(&C@tIEPCTR<>LQo+~h~t5KoJ5>flw z7OS7GGCa*Q1&6dL_}4cV$N~@6a}BXGlW9~wAE*XDOwN}##oUY>`81gQ6c(S zyH*RPwZtIeU6ER7Oj1icrJvQ0>%%pjL^&xuqV>jZ_>RtYh5A1)<>e(ZjbgRYV?%MN z*McR6jX*A$u~S&{xhF%_4XUkb6x919K2ACHZ#CD^-{a>WK`1COUCirpgXZ04(8=5Y zqgCp#kyGXas+fex4BfO;Jeoq&K{+D%g91Vnf`7m%1wH_^?r5v>^_ymrm$o`RO)%e2 zyRAaiW+N&95Y7O42n$-QOfUID&Bmhu+#1T~Fz;ub)>0{fI@+2~?xt(XiN57dD|cxA zP-Fk_Cm}S_>HRcdC|8A*CHqWy!xc`L4_=(Z#?qoFX{ZnP#=8)m6y?pOF8Zrxjoh*h zl!k#*pDjAGX!|&LV4~S1Qc&bkkylLB@oHzMOe-xb{S%5oZo26|^K3x<&Kw0Eqs6PX zRZ?)MGLbU1TW*0_^8~uNO1x}C0B{~cSQy^B1ILy;Ga?aeMqwvGVK7=M>)tEpZR38I zp`~iN#YBOb0s%UbH-$884bd1}1ok7;?B;3a6K(2onbt5cTF2Ht4U7y=wkiD4%3;== z$&T~;3td<`uu{4+OeNOwzSfnAa|oV*dHQ^(8ChWPOrbm&m}Y3mYLA^6kK$LJa2Q}+ z^5O5~%6?mCv9PoWspJ(I+q!ngsk3ykMtXM_Oz534$)R6&k2cq&x94#E-TFAexIx+c zU=@o`_G`CBE@6gI|Lw5ECoOKjPKm=?Tv4>j1dZov3EH2s?288qz$BJ5spirBdnPst ziN|aYQi6j(6D`FMT~2b9OKbyGt}*GZe}Ei-&HV#JAf&hwuDiT5fZzL%3cvyKOuX?BO1+DbRAXqApW_ie`X`0l>`p1b?7vl0 zh0Nx6^X2+;z0S5~`DEPW;aleQ~`Sy&%%Z&TL@J5s#j)5%tWW8S0*p=f0nn zVY4h__-f5tq2!D3)DkZ`H+w*~U;VTKVqwBVUCah?6CR=?82QS;g~**`!P4Zb^xBkE zOiGB;{4tF<=U z>{u|*ente=z28YNs_X{=8hVFM{K(?(hiX;`!Y;GL70@EMzza zk#6{1pZkAp&`Cm+>mqlr-44(L9l&$25zXXw1p8oSzMpK5maYaE!4uL>9S%%9jSIIvf*BnRnJ*et zzACttH^7XEQWJ85ue+@?dXyPQnQ@B5^Wy>TqMDy@gk5=?tJj4FT!axZd+>hiFN(3w#E?z*7LR04nc^6M(cSS>8c9`4CGG!g7I7lR?+-ooDYQ5$P;&P;A8p6U75)?nJ;n(^cf(b zrCkYW+vO0Rpcz@ew?F%|VT>^1+*wF3+gp?C&B&W>Cqt8xqI%gEbaJ;Sg(Eb*Y#4Sc zFXpDL|K6j2^}8YVAC1WZnP-Xns)f#rpkwW7>4)2uaD3LkO*(!)gu_^)Tx1IY`7fL#F1iwTb$+1XS;=j`3*r12J)Iu+ z{yjD+<_s^;?s)<6owYyW>TD48=X*;8ZndwN5>Y40qX$*_pN=*67b?)AUkfwnwFff; zf~C@}-i|SrT1`=+L-(S#^9(6+!m@J|)c8tcrmht?X^Sfe(Quh?RYRhZ{mpbsi0-rp z)K72ep^g7pGA`JEI45Zjj)yUJYcAUHjVDTMIXbznf@)bg=usrNAX}Bu6T@nLrXg&P zF;}q}L0_By;G7EG=o)s%jGc8NBRz9ECq?f|Tt!RvalB~M8WOrh>X3jF5Qh{|x3tnn7TlE?!0LO@XwWiWm^A@l zR|x6X(1>lqQAokij$lZJ>F{D3(Ldgf+ut?j=VGXk@NDf|Z1sF>O_O=UV^dOqRE{7M zDZI42O8!|)vUPEF5~?i|5w&MrfNP?`+F|qX$KfD>ZeVbv z=zZ@_Duoy|D~0~yA#uK&QW);ZQXqDgiRxE9s;I>nB5wc8R}&^uQNw})JW_mOs$m%! z)Jq<~vvJ~v6=JOI$VAEq%lYAowbtC+JZWSO=evS@4y(;(44yY448oOK2a50Z!zD>) zh_N$L6N@&{GF5-iLtz%P%fQo*Ah+@S1Gej9ZHK`kj`D>Q!4P-b|MiqMnf(0%T}ZGC zpF5L0O>Ta;BRhL2dw0F4R6-5LR<~ri;n-K%$k^>sMIysw5fwYdMPg0Z7TO;PI8HMs zmE&k}yKL(5?t_c9vHqmctI;XNXOzi*W>{w|oi9&0c|cIr(ZQbox_nFhsDBP%{M7I4CVjr0=%=-M1eUpPogKQG-Y^V^pM}b#F`O-y>BiRI7C)rt zs)c7x`XOaH24lCo_&iitg~()p;)BUh4Ro(GLv1MucpFYQh1)~Ah)2>?6 zZdtpIW6(XJ{GKB)IwOd0{)pByBW>MWp(;ElvgHtSSctR8NPHH^Ur)7LQQ^|CsrNc$$HQ`T$4pNR{Q zOvg_*zoQXP;+L@n{%aT#4)srQ0$0G?ww`9q4oh|a_5}hgB^FDCj4`|Z(!rbxDaC>rkX6lesXEZR9 zdi16ztHBr_Wh+$rb@)$0p`>D?u(<9ICf!ds<8j9hus)ZuMkeQJDZ{>zBk?c=yjoI1 zVGrwn9p%kW3BpsS0-Go&asBife@0WvwA>d7jm=E_l`|-I2r$Ulw6dE+Pi6y&@tDD% zi5sj`7LEoOSI$9>Lw_!Y%6P2Kg~MW^`gM^;`s|G`9|R}KU}2ZVU!F9Rh5~}o@(oTy zp}7o1!_vqd3~DmdcnZCv>H`alqaW4xWoOGw(~RgwO7_oE;*&Pa@i?-`{mb496XTFD zE=;tj`(M=Gt*3hH&YOhh#ri&8h9cbhDH%PSIl2Tk|`1Lf(F*Ebr?Bnb_4N~zDrtX z;2~8c5dFFVrrKN!6YK@B#9}rYwx_;)$!(Q7rY;JR0JdK@E!F~ zr}%Ze2{&TsH1w>gSgu&-*dVWepAYPHsRH=0+8=9$#cV4G3|UrsrM5pz4eEEI6-3RS zo$(Y3zc6LJcs$9k{|Xt?Y4_g!Iv`*VcFT+$8%W#FCljvu43_FAEWfis2d!?1?-QJIL3y||zchBtcY0(g{d5pkY z!1LV;)-Pfu0a`jzA(2KE&(72RPIVU^|MW-)@aT#2%}spDil$A=A*y!Mwa&vP`|NQD ztu2Y%rXn$`{=m8${_l~E6oHNLK>h*q8CCy zwM4=GqZut3{{yZ-Vb)_PmfgB|Tbi4ibNz)W&&_Y`N8#F|qbbib%ponWSfT4tq2rK) z`IB{0VJ~i7@281Y=hf3=uaTbSwWSvdZ@%GhtPy-NpUv18&&LjKV>@Ot?#|FG@A3EV z?pJGcHr)?uybB-c&R@2{w!(5Z9bJd=h4bwD(PVF*Hf7=Paj^uS zun>;NN5@uJ=e>OIu2$e07SEX%3RAR!bqyn}2td{Io6yNGjm<+w!lN%I1{!cpx?vLU zkZbR&Z>?9iI#{rrg+9OXaj3ZpW8^;y-EO93c>Jb*I3W>AA`{8WMyC9$;xpj7 zH;SiFh1n<8e0`-AxEH|oY1q3Aws1i60;``(oo~hX(vL;kFtc$(7J}-IP<9wJd^YO6 zDTztcb5rcJ!=7@MQs%@MG|Qhg!|s@hS4UxIWgiZ#U0?JI#xv%2BVHVz=Z|NK+h3-v z3M*s6&x-6(x$S)N0NijZ;&3-*b6CA4cvd* z_EClXqT$|KXk2F~%;eDiQd+vZrX-m3b~m(LgI~R@L3Pz?pjpkJQ@^}S?bwa%dz{UD z0I?hDg1MYIs!Yn(b*)D~+yIL#qO=t!;pD&F-X{ei0mc$z zHGg9?V_pL-3#*)u%V24Y+6;=aQZBYUl_!m^kOKM6jpmucVQ(xU+7Dm9mfTE(Ps^u* z6k!+ZQ^wVEOfC}(!9EgX0olF+;IMDkM&LKw&g4RUFNxP9Ti=u?GGEkO!`#c-jTaSC zuSXvD%>%PtP2Wc{YJ*}8&PT_}SGm`-VKd{g$0&za!kL|v#?}0?i{%!N8zawjr0uTa zOrh5a{5b}t1VOQxGRcPfhNVim{$$|v{=Hh__xX8`oP4^`Z9`TRs3S=O3NUR(U6JSH z{x^xXmMSyNjr5rZF0i7ex#aq;VE46y)RE)f=jCCzX_H5_z{7XX;`T%vBaw`iUG!f^l%Ye z^*vHtMiDYf5E%i0oJAy83X3M$nyePZIqdf17ZE_6L?_WW!&ZOau1*Ok&yiC?dMAc)2 zlke1vy42_$=mjNZTtJt*`N&ozYr%?C)!#t(-~#+NxSs zymVjV>^D5|fD}NVC6_gC&}W;QE!b0O^Ht_yJ)zdGW7lBDXUXo2hiPpary0J9;UW4n zWzB9T^)E&uxTZ=}mKo?zE(})Y#pY7?_)Ghqn#^(iLwY_go$xYImmS}$2hRQ^3BFcv z(9ilq>zJeP&-7e3=&7S0rkh}I>Z@(_@BPU;-H8*73%0v$k=2c~(3ZwK_{q8V`q{_l zk;Kgg_5%h^yQh+Tcv8;tfH|di5@tSB*v8)0@bJ*#KJhnv>`R{`g*T zuY3un{jUx&AtuvLBICo2sQhQ!+mn?)^wLp0=eY@6q$_i*3}V zbQdk(vn_f2E559N*D;n~*Ff7gm#%&D z<5WAzAgjg?Ka>$KeY-tE)ldctD~<&AcFU>XJVd=~>UtFvU>Wyb>gqRT#=GH+#sydZ2ylBC3`s@;_5qJX)zGu(cc0sMfKP%TPj|GVPGF*kyU6a7ahRqQeLzc_WN7xoz zE^~~|*Myf?_XQZCh6*`^Io=Ns`@<{e-ObaHq!7rS&)67aeEOQyb_L2J`tGG9eC?wX z$xNAnywF22dFfQX_h8flRpb%T+-!0Al|bW~+uUx8l7s3G(q2jm#VC3E__ZNiy5f^( z_QK-&t`%0@py#7@EaTB4Biz{`8#Ahh3V?)YPxa(50Xt#yqMFGciG|Y0Ix9N7_quTxKEgo8w1d@aM3EF=(Z1ydBmO^ zJJwLqdac%8=(c}{<(dZcq^y6#5_Tq*8!Nc2G=FH8h(VvDqA^=X`1*k~g?wt5u*9BA zBk|Y#A?;^d^l@YQHl9oy@*0s7C`nFhdjHx+>)m$#&(<^iva^G`8cRh` zU7wq!R~J%X+Vcb!|Bkf>Qlmh)gqJI915jBC#LzuC6TV+ZKx>=BM})txI3ZOS9Mt%5 zkFl&{K*U`7DWxWgzJ9*Gl)=Q7v2l3vr>Xh(>ly5ZVu_kxa}^R`z)a^%{(}u~U7@Sc zgS^7nI;~`v(Yk%|#%h~2A7Hir&uxRPo_FB&@KEeq(+OP(k1wmQ|MN{X-Oilr#zNcX zZc-Acc!W3kp|?4Trx)zK@4a^C_)%;u!#j4UOYnpUXah?RJk=gD9M|n$mp@iPrc3X1 zE2gHU9?EIsx3Ct-vF_yu*BM8PvkLJ&ehJ!JQw1Yo3dez%zOl!qlNf>(fFH2a*ObN% zZk(O97@9|qU9NcJS5Ewr9537K%iJ#uE(>_vAPciCCmJO#uXdJ^20pv)JV3Zr#p#Dv zA1TS!%ny6g>BBr7YcHeVAV*g=B*$!FPNsLqSr{Kf`;5D{!M2Fc~~cb09aub4CrisGDd)H#UD++ zlrJyN)(n#H$(EE_8jp-;g5Xvhzs<_hDG^lq*z8?5T)8IfNOWHRjp6-bT+l^f@AX31 zym<$EWSO9?2<8yEjg^4Id$&4p)`WGp%L?yt+YSG@Y-y~cJ#LH}Jb%ixtVanR)gfOP zZPTiUOk8c61w5r7zwkO9;%X7-67hJiL>d*MAMhINGQalc->FQ}8q^k{iuK&~Ms@XZ zpeN{{p-ELWL<5Fa#=fP*&l2ZN+x$G-$wSr9*1q~J)^p%*q^aD)yWZv_MAKy%K>M{- za;C2FY1rmX*v1t}WWvW?fUmki;0~+L?ANcb#eJtcxHJu<}=3->A=-1}DU zJG8nLn0TF%bfE5Mi%q*Z?7Y~Px-Y&Fkf_MNOF&D_GZImUpH@H$r;-J!xu2lW*7aB4 zc6}dh0@RH&fLg449LBunnnnh^4u$2cvxmlqU=Zc=bZJ)T6asAH2HHr>8pjYer^sW= zXF5`kgQ0>64T~^bFxlivE)R=wRCC8?OF{~7a+(zZgo4^N1{lgwA0t9sf?7`W1(inJ z;w>fys5y+c+4FSSh550Q(eUYo#1#%#N{N1=Jj?~w^Q86hm$F^t8tso<6x6Sjt1n^s zKbc$ihB{?$oS7$ur8O2cKpNle0nf(3D{Br^P23hskrsm3QD`1~!aH6BSw!ejD4=5~ zS47@ghmXIDjCTaC=B)WA&C8s)PgH-Cnwy`o7Yegyla2#8NhktMb&DCyJIt&u*PTZ& zA8Sw+-Myb6UsMG9inlyCq5TuQ&R^nHtm1lpul>5dT@B4n%RDY9WzxieQUe2Wxi*>_ zjWQdmv2ObXn=Di=8BL}ytV!GHM9mh_k#fEAZE%ifYEMYZ%Mr2Y zMwGG`R}`1X_ZWSft5?=h{eGt6j83j!@0gHCS$|YP^wjJ|(uTw}t*~iXYvgmacXeCk z04gRO3*rFpn%q8Z$!xA~Y?xSvZ3QgYOs(j}*KB9o@vk?!%vYU4V>~+*(GY(XQ;h^A z;GGI8YOc?mKo*_}H<%~aQP|Ysj=pIoQ?6Pz&W~5Xrz+1x%ggqIQNMis32WR%gj5{9 zW#H_Inpie4Jk8;=x;O;|B_2o%5CfOFQ~3s>KT?p5J;=)Gd7So6M4ojVKNC(4QSE5Q z&JOJqH1jD(c~>jlDu8(hf2Xb!Ii}gJwvarGSQ_`F8rADJ?)&H>Ge}OOLC)qye{vbC z$ISlZ@TBuM#BwGL^>0I*)~4ARj%9|dmxbfOSxSCpD+FpJ4!daac3_QRnzzF=ICr=# za{>Xr3i2A+-OtNww5{F z1xGOrM2*~X{08t~}|M8u|<*)gOzLmsDU&Hv_SLE`| zb(s-1Ku;sf2JY^GH|=@#j{U5&6MK_#-iBf&!PJ2%{Nz+yc7utbmd=-ukH;QUPNVEF zc4{2mL`v+6ur3Z>A^ZrGw3QLdC#|rK4yJ6XCXn_zHHaE1m?uG>wD`(O=-MkJ%O^_<>8*Efd=3N zEy*iS#&z})qN^-E;O&@DG!SQ4uZYI;%GQy!1_j-9Jj#C8MI)%3eBB;qGU(os6GUxZ zj2snd-`{daoKNJ!ScVf-W%@3ExTA2yU)~GE^yi29Ud;QTvEhx~3g9wpN2dHHzg|FU z=Ek%6wdECsxmm-N*)B;?N#sRJEjy1{{X5qi$2kF7PeeHY}GuPzfov9SUhS_>mXMpw{DRxvpD<4b*?^v>X5KbI%usUA!?1*% zPTv=~he0^s1bouh*5{Ybw@;dSUb)m%xDkL;jPK=w+=%;a2u4}{EWq(@oxIyNd*aIw zsj1)bW?^rk-wR6~cbH$f;P3@hhAn-1$@LQARMN@#~%2wRh&R) zVRZw3yK=qkMw=L(L+Yt-ORFyz9pF{q6a;^+2wT{eHYCK(nMtV zcn*X+PW2)v8$Af-14-z#w+n`<(zse2;!{qNUWyv^5L3jupOutTO`5R*M+V<2k$b4= z6{$bm&6Oue9;3ze52KqEc6&axM_+Ra;%2B4@HWFnU$2hc3oye7wjPgm)-7_Id_wE9 z%8ZZauKoeX+92f3Jr=y*WfqB3<;VcpE!5|iD=p<|hJQ}eB)_|XtJC@UzH3?iE2crT zxJij=d5Rv|24}q$B&1BffA;5WNAU;jCPG>~Qc~IpAv>XO#g*}%K2Ky0?D`fpF1GMv zZL1wMCWDwJDXqSPceDCZR7QlsO^tP!e%c>|)3FW}d!#>+zR zi|EUo3lW#X!^Z?8GCr?G5k&66TB55o{IjJz^xOOUd$Bs#EaL9hx7Q~uEo$9Ql>)C- z*Cz=-(-xNBw<^@SewyxP(`a>g@=zC}SKg$3%A#;$u$B$+K?8IkxR26T`Joof$0hT9 zWP&MOddTtyR$qkFdb;sE{3^c)+?@*(7oTY|NkdM4fVzKlN8wHzwwQNzP;xf8ZR^bB zP`ZiI-{XrGN=yYQ|gd(5#dMx7-Bq=W9A%FXd=ZkF6dX z%Owqz;1@r@+#~y)Mj6Q7-nvnn7>14B!tI&dd6(Tq0q(ZGulU^izDfQ5_N{Czx^h%% zJU-h6-Cpz4f)67s+d~ry1m%1Wt8gNN$j1~^(llgRVMuu7-JLcQSGDVOe@K-iJ$Qd9 znY!$5a}AzBS0uaba2`=nYSk9&0$q5kLy~EAw1vEwU^pixECe-pO_(0Ns}Hp=f3ruJItJX`vzrdhHNNTA+cCwrh^@~zxl{@+%v6Ys z+d~I*h#U4-x%6r()x~nlzLBkTW76TQ z$OedR{zi}yNXu2rq6TX0wERti?9wghGNvRYSzIPe?2ifYGZDX3RSG>{VoG^*av459 zl{bEZUP(DvaguEJbO6FguL$k-rD!Q$C0(0C;jcYPnnO)GtE9TGkZ3U5I$#a zZ)clPxQt*xWN_OwYiN{oJ>apN8#jfGM@;wk*T%`Zk+~1u43HC+lNkOktcwp&UPWL_jmPI+f(4vM2g}<<82;H6AA0@wDyI>nGwx-aX|w} z+Di4prr}4!?r*32LXPksz&nFSffItZcNr$MXTwJupCf;zT%lYGUbxvjPNz0zj75Wu z7Ld;Ler5S$4GTGOG2xmkWPAtVa!j~3CLQysSEuj#pJ0?f!4NU3>Yz)q_cq6du){=N z>@9yu9g9Q2pxx|y6?@l^SV;?k<=^-eN8c%I^E7r=|6NMkz4>{!n(jj?;~v-8Ad`@7 zEoAT1eE=mD7u7MK_JZ=Em-#gIK`y!EU`>Fb3}sXC>gIETA~%cW1~_#s(x=ap)Z?0E z(qCmqJRRzQ^u6>kcl|pX%g=?hc~ADRRM)4?ecHW}=Llc%kKd zF27-3L&O6pHqeCSJ6*&@Yg!0EM2CsTgNX$vLtjY+KFR%hm#1(ihGwi_DLLc+ajzp= zZ}V=a|8=vLM3cmU&S&wVHuHJbg?*oO-G+Nzef4yLvda-2I@KnCD{$xdsH>-wr-3l9 zv5&fu^2)uR<+aH>wy|;M#UteVbR72L>9Fn(vSR7~TrX%FA;g4UsKE0%2d%T8{+NCo zgX}r;sFcp9GJ3%77t5aq9n0?bfbvY($tkV*yJUnZ4^r)LR1}Zoo^L0_-!-#ES_!*a z$cS8U`~rQB?=$?7HJ2H-&ig~U?x6g3@O8cLu_XqF!5WK*z72E8J++-s`fQ(@dT0a6 z&TNg|GVM7jh|ar%V_WxunAa{4?^Yu#NZr$Pvo+AO!X6_y|b( zocpj(BZ;Y$tY&NNZp(#j!-7;v(wX&c;Nj|?&*v^XJ~{pejiiMDxqJDC3dCRG%k>%x zfi8-B-s-9?$vdkXM0q#A4i*Z610(YAu*%oG8CvhM@XOYGTviz0QAQ*NVcKG$PZ+sh zfSSL%i>#3JeldULgbtOb_g!gej2)nw*MeI+1O`!O%^=EB^ae<>hy`RD)Ll>ErVas7 zjRRkF%N#dw7@6j|@Qsy5gN>RP8aiSI_TfJO9xLHKoJvF>J^d!z?|pq}L&f!bLSL45 z9}CwHdkHC0Y9-t|%ZKSrq?WMebH3+BnEXXT{I$10Xhoo$&b#?^4AyE5@f~WQ(u$7E zD6*BA?2Odgg;Fyf4ad=zW+(nK%gbPvcMUUk8}AC+^mLWmcPG}?H9WXc^IPO!9+*QX zrE@KNKDE;8Z`Y1tx7>83J;7qBp-Nd-TcUxRdZ==6;AXpOV_|Izi}wMnuZ5-l%dY5` zjg8%vV|UNtCVVpDmMd>-qWuewf0JrCK)O5L6_-&PK|fw2%sq@QBlkcz&c}%6%nc;Z z?dF-#H9|u8r{=T==WO_lzdp<}sn=d}Allw(djDOX87w{is4nt*b3&BuljiBP+Ij|G zb=Z6UZ$n>vTPrxSMOKKYKl;qWnCX`k%cKZBm?32SnMMDsdtYp0l#5mrK7Z=du9_#% zb`!5`e){J97WgUnA^x$4P-pBH81GLr*|tzdGug?DKsju1f4@U_jLr0K^7;ELJiw9p z*?u<=W1>G|sJflRfb4w=^=?r-UV2*ThnXIOesVLvWVD)Fs^1A5T+M4u^Y`87vNH)1 zs9P8Yg$J%CH^W9J8iSGk4Ry~3v7!4Pn|k&!tcHiKJg=6dbEy7hOpAFgUs)D3rBt6! zJp;J137@xnLh&T{Ub|eS9$up-L_ktQCtJ1)OTjG@f5Jg{%OK_I(r+ugf1{KnK;lsH z1Ej0i+73>!WP(lw9HxYlDu$nP($n9*VOJIl!+;=6w}|F{`~-K6LNNW6yb5&f<_S?3 zPmirE(L3-?*H2!jD(^h+l4J$;{#e`=Ok^5(_Dk%KC{=bU(nbo-mwxp#lk>Y9U}&W6 zajzA+lT_Bg1ki+>HJ|5(WzgDlLe`BBYM(!g9g2^ zC)dEI&7EqiP5T{Ezu7g{UDv&1n0Sv@5v9KAn~vs5!OpIJT-s^skUz?RZ(7WieE!{w zHPo2xQ#8ThbILH;G7+$;FjAQ5+3O9qJ!>!WUUz}ZMW-%(hx^C;lSIZflxDFQAG>4{ z7X#i9MW#*tbP*o!n!xxhTvf7p4#z)yZ%d{qPEQXm0P~BNw!Fis7@3o|ci^L&7(nAR zYH5aTW@h)N6sfWXXD9J&XZG{L2C1@I!&3+Rs0qDhDdH(g+v(F%J2)umD^7e$71`ZD zY&oAzoBkP((rI=G{RrDOfdTCqPknawN61;(abUJcg?vYtGCjUBMZ z?2~EsXGL)#j_v48?Fc-fU;n?R&N8aauG`v3Xwl+c90J85Sa4`@0u+k76C6_9DTTJU z6SPpY#oeJ$+}+)ZyW5xNocDRpH!?>4-?Hzu_nLEF%X)Z0&ub#F9R}8;q;V9Rl0Vo# zKplZaiGiJ%;}Uk7eII>uhc+4YUTEw`p>Y%Ai_0x$F;%}l6ShDU9nY=v?iE=!x*a>7 zIX|&w>(Qv-pHwK^RvIu4$@MDD<&m3GB_gGTTDeZ`g(hxJf2Av9W!$* zsw5r{kIzPCRz&~C=Qkv;X#kW)?&dD=qbUR~XiZ?gn8&Sh)DjV5!*`49L12$DGx#3! zr*v`G)DKIfgs;PaH)`qkA7e0zNoYu!H{_znX@;m)8=Tb5f{Bt}XX>}0F^T|_8`i$* zrao-pUO#G*aO!hb%W+qSGlI6saU;Ja$xqGzmD*nAyW*i+TwFevzN^V7cMPGgfc(ie zJ(#YJ>Sg1yA>fb)p4VbBInOW2tE5(zF?Wg|P_H@#@ZHBy4$X)+#5j*69__mnK>hS!IAoO1I~08TgrW zB_T-`7nh2Y{eq|&G`SF(wOnI-HM73}so9CK=dtjsJv>XC1Vjfo=7z@ZB8xwqYlmbz z$c{zpM2E1a>$B;2W}*6aM^hAuxdfda{+tTq;FfPTPaWT~o2H7R4K2PgmA~mwR?J@@ z8Y5J%-@IG`TzsKEA1AqjCQ5m#zlFd{`L0Z7iqTMa_Ea~dX=9$8m)o0T8|urO;0=#j zj74<&Gb^`uFh{GE<19e^=nr^0(qTy~WZ979v*hOruQU(qTAuzbc|OjIK8>+L;uEi% z15={#jKnd&sLA?A3|vGvW2000`=Jb!T;Y|x1lxdu06iM>+m;;l@z}wrTj#4?~^t?o~73=dPH#9X(=3b`S_V82mcAagH?Yb71d<*xJ zn9A8g;G)4lJ`H@kjDQra;YTyG9Je4Tk3gS&wCiA>i7re*llORN^u}U~uaxEE5&q=8 zrhMWb`JM%~v*ixGS0gLqDD;zm8UoMi+-4yAe7L3JUZ?Z?A_VZIr;NGbv~E2dkL?@D z*N6?Lv{GrP?BP)Qhzko>KVuA5sN~?v1^xrUD*yTI%z2BEKHt_^kB#L%NuvmLF`pFM{RXw~G9@ zb}m-^jX22j{OVBJp}xBKHO~l(^7`6+?Opb9N_$_*$$-;s5&I`u`${=c)Oss(E~@(WS%#@u^oH_ecCMA-CfMGzIC%MS=Qw( z9zV^dG+=J@zb_|W7rUS@Q^DIazJ1YoOwndQb|R=hzHU3M)vbGvwUbj`HT2bI9gvVg zRaVsl)?^q6lAfK2bYF2*>kay?M33bO7c!U% zp$I8M0yC+!#pfon%@zZ!%yMTAMgkI?0FUrre{4_)=HJPMD4o4Wng89sx;-Qb=~U1^ zc_?W-2L+|!YKzb?4sHJCTRBSjyem>p;LIC;-e~4{HmtWO+fHot^6p~m1s3TiScTou z=kSB8-O@u_0l6KIr(2^1I$sFpR#iR~DK&jjMJyTKwgEw23^50VTQxi&9X=+XiuqX~ zxuK^hsA`>brK*{c^0&L3Kb@<`^@JJSQQ*)qloJRI3!G_l8m2=Nyb+@eiUxi^A4$)r zEuy4)GCd>iVD?*WdwIIwnDr=K8tipu-S*7z#o0-LeDDfj!rX3k4X}v1MeBA~CEF0^ z8(J&ou5vvT#&}uoEr9a;7r)bDs5??!>pe1r$`Q~T%5%h+!_V+4O+m|mmYVCm$u`Io zg2>~hcEQ_$_;nnOp>4msz5!fv*9njMOFgjlfOp`PZwNd>vd}5l~jiwzmF_fNfw}m-P4JG66mbF zdl&W-p7d2*r|%a+tEucMdZW&u92niS(g&*MV}7jq>eWfy45l5LyMPcb>Zo;swBYx6 zy+u?SAV(Yf_58OK@Fk_+!wdwfaL;Bd84-(@2nVJU!L-h4cw!ggP$>z4EZXG|MvM2d z?&#Pn;i+u*aqhUIoW&rcsy+VF#8d+s&v^G80_rouvQj0uY>Wce|F~T zj|r!$>@5jqd*4Tne%sOU^5|3jfoNX@X5xto-k8uxru}a~0-wm-(xBKI!t083^*Ukr z&Eh@xaU`Hw;k(G-rpl96_{pvVr&z;KUuDmIz{#e@tNZ<3<=6|C1^(f&c~;CSI&1C6e4gC=!0B zTc$44fJvt$KejgMt`dus5e2RD$zZ5$0QH(#TerBr`@x5jDY1io6{2a%+i83D4oNha1Faxg^+`KwyXoV}=u$HR%f4|Tt= zFoGX4!sedunUQT1-0fYL(-_6ZMi2$jZ$Lt1UzdB)m@2mA0lacFU&jC*cRR57ywUuC za`@!3MnF!=RrI!}P%GVi5WpXvOixMiI8)Pfp7+WMW;>|dK5pGXG?XAd9oH_X9dSvQKCwZQuhvsm9Fp=M{`CLe_=wL2{)%I6PI9d6pg`0d+KkJ74 z4YxW!tFmlTes4tRAtmd;pzsi&l&3t}ll9|5e@g z&4Vhz?kv0^7!;qC&Df?(lW`@o7}^@QWc_ZuwARdwGaL@W^Q7ml!;A;VDunXurh{Q= zUPs`Rf#gP=(^}lQjpCjblex-I*|yl>=2t?gz57ZYO0U_;K8l3Ua%Pkzn8OT9tYpn| zC~W6k*0SRWcl@ZAyT#NO(#aN{#91D|FAEY10Uai&okzVfThYyftt-YDENl347>Dx+ zkK4Z6Z<{)ax~h*Ah79Onj_JcFtVwrHiUW#x2{4n=9%kTtpq-XaIV z;IB_~gFD(I&GK&xMbRW(b=mifLB0o%BdwC&V+$65Z!t5H8&}(G*P_cuplC~)gMGoj z@qRGu>G|!H%*Y~b-KM0nftg}WrQjWL10-&3u=!MYEP6#r)c^lKsP9FGup4-$B*2jgK3`gIUgf;~Q?! zr3*RXE>Vjgx3+#3Rpr-cjJ+BeuIGx*P6}YSs|Tcl?k!fZU!G6Pix2b<#z2{~7`d8H zlh~r`lc|DQToG>8&4~tM8o;G{%H#%Ja>!y(rF9odBC+lUyN9AW%~$I+m#@5@-NXbL z%W@qXgi?Ey^bGM)jvQ(HmQ79IJlRX;788$=_(zc`dMn1yP+1hiL?pijcPz^R<)aZY zm3OS1d`+oqqgf5k#shLr9YuBN^=prXP>Yz{eoqhw+{ZtS?aBB8KL8`gm~~2O!9nagGRpqGMx!S zoMAD{35FT!&>Vywu}m@Krt<5fQ_n}Mv(;WkSX8h$d`FzbxMGM{m>t`mV?s%S}HpFX@e)X4==y(09Q zcG2K3O}XuXlZ8}t^LO1ce(g-4{9%6JqjX3m=-g=U@2>d zfOv!Mijs|{=qVKnqhtavr#n_(zUmVFr<-;=V6Mt?G90q~7@p=B`1MhmgPPa&hTG=q z@iO|jKt)_?=y)+q9NMjNK6@?xsC*t;rRo$J*x$S(Xy}*69@Z78h3W+0M>EpF9v0fN zAfku+jV42A1kNib9LlY4eM5$~KqE5EM1I`9b{wI$7(LHOG$vRXveQDf|NslAH;& zwEQa%T{Zi`y!{}GUg16jHC^+MSouEmDOxo;7SSn3`ZI^g$AROfSZLO2-%UF4VHs1M z!cH>TK@Tm|tKuTdds#}F;-2=?-PHAR*p(XzrqlOn;`Y(1*!C`L3+1reeSIeq62eox zXv&wa744q(aSP(Nwxtg%gsKArCVo2%j;Xq{ItLpG3e10LRtkVj8%^{}JI~Ysdg91w zEyO*QxFg&kJEIFP4WCwW3IW2>X3#xI{~QdtOU?)=?am;j3khQs6|jmL0G}#13)t1x zhkjGvbc~*H2Iq5ZSznq9PSTh8z$Y2R>@m^pdh)zx?n)ZaI)BB(`@S+Fx`ZO|HLrLY zoTqgL%^TX$w$HmI0y+~B{1qX&%qkyPf27IKoUv~G z^~#vf@C-C|ZF3%W+@$ecOq5A)-0ZtUw1|R~F!pWvwSy#wt;L}&;4e2WcxY|S1w#+5 z*kTK%k3s@X^Vf@_-E=~eiWsU}wEn@?i$!vy$LYmr;9lMd2go<5J!7FDI<@9DqfWLx z&nY!qdoq*!`myX<0*?gE>miG4k7fl%*#@&@#+ordXiwcx8`!B_w-z)go14${-zEOq zEwoGlIqsn+X$eq?YQFEY{|b&#{+#+z*}L{a>=b*h$sI%I8V&tkQB%W*Z*WDqG)~yJ z*eYRy9U5ajJsZy`#$ni<+gMDx+>M1;g|i4}SaXms|07`Cz=qyq9FR^z8bl_6=QrEO zi|3an?(a#k`e*|GhU#+F+#~ENgI}K!SOYp&b8cB`FwSJ#pYRb!MaW5&9TiUbq(@Oc z(XP&lrna}sez6J#k|M8(f8|@e`v)_k$6NWz65NTOG^wNejgd~%meqR6I<@OwRm_;b zn>AO2%Jp{*5=-Hqm({j$&C4&yu7gzJx}$qiqUY1~G#gDAwSva5ytp>9w58BExJo3@ zJ$1E6>^$0JSYjX42CHbu@Wy8<8asY`nJ}@KnZ*S-kiXc#+0@kuIbBN55PX~IzonUs z5om?Pm)iX@%O{U7hhl!YM(NTspVLxLr$4$#u~V%pJe-!`g^nhN&p01goI}xUH3@xt z8xSH^WU0^V%G>kuPRcBD_xKm#ZV1l~0>c7ON1+NzYBPVo;I0=z2*CI-C`^KmljOgn z!IimHSaS9{ecSE$_*fWwaA{-##gSm|ruA2(UpW_Z`;~{(mbNSLHDSskvx@keLUl-E zQLP@NrTRT#uTCOng|_@F^@40;j!3u4%TLJ`4-NqUO0P zPxVo!pD=OpDnjFl*@Nc4JjB-&r1c5EI}*wyq=o1`w~rs z;9Tt^wt6ZgNW-`9ZPutBk1kxQ)-&~phQR|aF6&@x|80ZaX-gWaAPG|Pq~j%h=|5wVVSw;aRi`OUSddLHm_z@<<(*X%lMK zCOmEwfws0oxf#XZrDh-S^gYXy1go-mLj7jD1m(xpxh5Zm77rt*wpa^*t!74;cb7U# zrloYfUu2Uz>KPfUzl!_fZ`{76?i&HfHDOgXnO7YSr{TU>Mt2u%Tyk%E^7d|uKmJv) zbLK~VEZ^I}yf5tYJ+ud z79T_xH8QlF=EINHyBbTt;AC|o*^hFS44KDo1>!ZQ*r-3rP!ISzw6`Wlh$=5SAf4?CF5N1!K55|YJqJx|aki3$LRheZ3ZyBoSZi+lAAZqV z@**Q!W}{rVIbLy2d*ybt`E3ctB0$oqohsOj9I(Q*%^_KGz>iLu#@Ti|=OFJ`sY22r z*HTujd>#-+r|QY}FxnFd)*40_|7^WUd(&wAbF+>7?k)PZNHfdKxw}(&=GiBuIN;xX z5%xG&-ftn*s9%izuEjXcq@MZNU|x2B1>wQ#ZZ@mm$1OCw+AHGdvmen$*~NXgBWbhv z75je5P#kqVtg~D8vqFoPyRHXih9BX>)yKKJ1UU37@JF>a(OycW{yBbT^^_%^p!s36 zx2m6A0k*?Ok9cYiV~UH>{D?#K0YV)N4p3Q>bMNt9a6}f={p&u>alL^an(-CisIbA1^k3B5<5{bg|`}(7Q0#r-7gTTD#%#o zai}32Ed;WdV&N_^UZ1`hrb6 z!Cg?o3An#4L~x$VB{nOShD*`(5?QJL?wnSbfLFkrI)eZ`7oJYA+M!Frt;>o(eLe)wolLNc?}!3*F}=i3FSyq!grW zO8DOqdJ!NsAq+qI@qCaa(a=7KkCp!ewoA{8SAV!=5o|9yz;r{1VSJ^Xs-EF z;E)4IMFR58BUxSnS_G6dEfyTQWS5j4Pu2JV&)D`$-EC<1yy(*Aw^}$W(|A!WjxD@{ zId`?YzgJ+@UG-45JQGttFNPL3>-42MNK^dmr${c~=#yc%xExbWYaH05FnvK{40`kZ zCd!|(Y5U!aDiL7J0>^Zt1h7^z>R8E`6O?wHgEpy0Ryz)V8OLRw=I39JXgd?jgBq9= zQ0R_*jB6bCg#wJdoF+(>!=Ahr)UskdEoI`j#f0=1)~cxZOR*+0644*bz~8_DOz{J50+JHY-gXK{L839A3D zcfEgMA+BHIYV!hGMn%4nz!Qa6*3Fu7UQzhQ=q~ZD5q#7R;)(~knlUl;#uqu4s)Up> z{HEARU!*jrd4y1}`TKgb9e?4LQ~FIMXt1(0!TtD*@F?0KZk03&L5(BOlrCuS&2F4q zL|GX?S6my#;kP+G$yHwiF99$H%a4=$=Jh(6A+nv4Y|Co2nCP#smnG3bsM9$940qcow2{usoB znoIG!)avi#rOnH%(B;H0uS_*!{_?*I}8K ztK)evm`3X?KI+af$bCqy3Y4!BtFY12M>!g*RkCuDflEns;=9XA<}=~M2k!3{%+d<= zwD66q?AmQAl}UNsXT#rQq7Y~qte~m0#^%vK$Gdg`+pOE*{haOg#3uq`yZ`$N#HO8B z{A@eZXs+B;aZu~QC+E~tL67HDooVYU`C(=@Ecu2(uAo&ucenrTkAgBTkz0|+{8N_S zf5}5P`J9?~8et-3T$!JioQ_70m!`ag`{Mjw?iqc@id-aR6>5#EE{6RO)k*m}a+=x5 z#ZgDIuj)LU5HAr4141KVSIyLqyFCl7$jA=v209ar*X&$L51+FJhQY@<2^OKY34MjA zEi~IJ`KgL%EJta<0D6Tlr^{$Se!mqkb2{g*%r+YS`ef?K`UaZcvF$$Hlkr<0h?c%b z9}l3d6bkHfut)TG3Xu9V%jj+-)gJw!M)Xha25;;*sY9iG+76mwc|^bqQYkY#0M=4V zr~q;r#no)uk+eYlRHfbT0?xqDzO;%N_V43S#Ww7<9WvM{))dJ@%^Q4qG(MMYeO$5y zeWSTiHGX!E+f)C8Wm~n03#^IJYc}gLov59i3)16$Eg;D(pc77M#d?wd`5KX4e=(oX z$$U$EHeKsFcech>Xv9y0_U=yVM`_G*Vad`rolU%(dfn9c*26)b__el2>5clA8E$Y3 zL^^D`^&!U5yq*xUr^OI2%E6?|iKYW&czJ7|`^IwEi2_3he~?!}|NiA!Vyc@43`==! zj@Nu18kd>MdOZB}L95iTK-`J#PXr8#8pIAeupoE49I?DU^C}l^{;{(lLZo2eyxjLz zY8drxOSV~7(nhi5U&<6H#d_DFxzj^rx?oP(;Ohi}UJY))f02Z~s*HGQqxl(+zZ1JE z8*>$>TQBf1zZ^^|YEE*K&rhjqqry(96LMb{(@vukg5Q=%uTC6>EpE=SNLN0d*IBTU zZDaf8&@9?cHd5ka@_$^F1Y6G+rBWo&84YNaYE7VZcBWA9;~}Y(q<+=Z2?(XaEl`7A<8TCptIuf=ub;EMVL<8?v%mEjK&hL

    9hCeFV+L;oHxjMVd$Yt@^bN^Rx?A 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 + {% block admin_form %} + {{ h.csrf_input() }} + + {{ form.input('ckan.site_title', id='field-ckan-site-title', label=_('Site Title'), value=data['ckan.site_title'], error=error, classes=['control-medium']) }} + + {{ form.input('ckan.theme', id='field-ckan-main-css', label=_('Custom Stylesheet'), value=data['ckan.theme'], error=error, classes=['control-medium']) }} + + {{ form.input('ckan.site_description', id='field-ckan-site-description', label=_('Site Tag Line'), value=data['ckan.site_description'], error=error, classes=['control-medium']) }} + + {% set field_url = 'ckan.site_logo' %} + {% set is_upload = data[field_url] and not data[field_url].startswith('http') %} + {% set is_url = data[field_url] and data[field_url].startswith('http') %} + {{ form.image_upload(data, errors, is_upload_enabled=h.uploads_enabled(), is_url=is_url, is_upload=is_upload, upload_label = _('Site logo'), url_label=_('Site logo'), field_url=field_url, field_upload='logo_upload', field_clear='clear_logo_upload' )}} + + {{ form.markdown('ckan.site_about', id='field-ckan-site-about', label=_('About'), value=data['ckan.site_about'], error=error, placeholder=_('About page text')) }} + + {{ form.markdown('ckan.site_intro_text', id='field-ckan-site-intro-text', label=_('Intro Text'), value=data['ckan.site_intro_text'], error=error, placeholder=_('Text on home page')) }} + + {{ form.textarea('ckan.site_custom_css', id='field-ckan-site-custom-css', label=_('Custom CSS'), value=data['ckan.site_custom_css'], error=error, placeholder=_('Customisable css inserted into the page header')) }} + + {{ form.select('ckan.homepage_style', id='field-homepage-style', label=_('Homepage'), options=homepages, selected=data['ckan.homepage_style'], error=error) }} + {% endblock %} +

    + {{ _('Reset') }} + +
    + +{% endblock %} + +{% block secondary_content %} +
    +

    + + {{ _('CKAN config options') }} +

    +
    + {% block admin_form_help %} + {% set about_url = h.url_for('home.about') %} + {% set home_url = h.url_for('home.index') %} + {% set docs_url = "http://docs.ckan.org/en/{0}/theming".format(g.ckan_doc_version) %} + {% trans %} +

    Site Title: This is the title of this CKAN instance + It appears in various places throughout CKAN.

    +

    Custom Stylesheet: Define an alternative main CSS file.

    +

    Site Tag Logo: This is the logo that appears in the + header of all the CKAN instance templates.

    +

    About: This text will appear on this CKAN instances + about page.

    +

    Intro Text: This text will appear on this CKAN instances + home page as a welcome to visitors.

    +

    Custom CSS: This is a block of CSS that appears in + <head> tag of every page. If you wish to customize + the templates more fully we recommend + reading the documentation.

    +

    Homepage: This is for choosing a predefined layout for + the modules that appear on your homepage.

    + {% endtrans %} + {% endblock %} +
    +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/confirm_reset.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/confirm_reset.html new file mode 100644 index 0000000..3e5a716 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/confirm_reset.html @@ -0,0 +1,14 @@ +{% extends "admin/base.html" %} + +{% block subtitle %}{{ _("Confirm Reset") }}{% endblock %} + +{% block primary_content_inner %} +
    + {{ h.csrf_input() }} +

    {{ _('Are you sure you want to reset the config?') }}

    +

    + + +

    +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/index.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/index.html new file mode 100644 index 0000000..d6e7439 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/index.html @@ -0,0 +1,84 @@ +{% extends "admin/base.html" %} + +{% block primary_content_inner %} +

    {{ _('Current Sysadmins') }}

    + + + + + + + + + + {% for user in sysadmins %} + + + + + {% endfor %} + +
    {{ _('User') }} 
    {{ h.linked_user(user) }} +
    +
    + {{ h.csrf_input() }} + + + +
    +
    +
    + +
    + +

    {{ _('Promote user to Sysadmin') }}

    + +
    + {{ h.csrf_input() }} +
    +
    + +
    + + +
    + +
    + +
    + +
    +
    +
    +{% endblock %} + +{% block secondary_content %} +
    +

    + + {{ _('Administer CKAN') }} +

    +
    + + {% set docs_url = "http://docs.ckan.org/en/{0}/sysadmin-guide.html".format(g.ckan_doc_version) %} + {% trans %} +

    As a sysadmin user you have full control over this CKAN instance. Proceed with care!

    +

    For guidance on using sysadmin features, see the CKAN sysadmin guide

    + {% endtrans %} +
    +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/snippets/confirm_delete.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/snippets/confirm_delete.html new file mode 100644 index 0000000..d4f0a0d --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/snippets/confirm_delete.html @@ -0,0 +1,24 @@ +{% extends "page.html" %} + +{% block subtitle %}{{ _("Confirm Delete") }}{% endblock %} + +{% block maintag %}
    {% endblock %} + +{% block main_content %} +
    +
    + {% block form %} +

    + {{ _(messages.confirm[ent_type]) }} +

    +

    +

    + {{ h.csrf_input() }} + + +
    +

    + {% endblock %} +
    +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/snippets/data_type.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/snippets/data_type.html new file mode 100644 index 0000000..c328439 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/snippets/data_type.html @@ -0,0 +1,57 @@ +
    +
    +

    + +

    + +{# entities list can be of different types #} +{% set items = [] %} + + +
    +
      + {% for entity in entities %} + {% set title = entity.title or entity.name %} + {% do items.append(title) %} +
    • + + {{ title|truncate(80) }} + +
    • + {% else %} +

      + {{ _(messages.empty[ent_type]) }} +

      + {% endfor %} +
    + + + {% if items|length > 0 %} +
    + {{ h.csrf_input() }} + + + {{ _('Purge') }} + +
    + {% endif %} +
    +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/trash.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/trash.html new file mode 100644 index 0000000..f3331a6 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/admin/trash.html @@ -0,0 +1,38 @@ +{% extends "admin/base.html" %} + +{% block primary_content_inner %} +
    + {{ h.csrf_input() }} + +
    + +{% for ent_type, entities in data.items() %} + {% snippet "admin/snippets/data_type.html", ent_type=ent_type, entities=entities, messages=messages %} +{% endfor %} +{% endblock %} + +{% block secondary_content %} +
    +

    + + {{ _("Trash") }} +

    +
    +

    + {% trans %} + Purge deleted datasets, organizations or groups forever and irreversibly. + {% endtrans %} +

    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/ajax_snippets/custom_fields.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/ajax_snippets/custom_fields.html new file mode 100644 index 0000000..90f7344 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/ajax_snippets/custom_fields.html @@ -0,0 +1,4 @@ +{# Snippet for unit testing custom-fields.js #} +
    + {% snippet 'snippets/custom_form_fields.html', extras=[{'key': 'key', 'value': 'value'}], errors={} %} +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/ajax_snippets/follow_button.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/ajax_snippets/follow_button.html new file mode 100644 index 0000000..9dc09c9 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/ajax_snippets/follow_button.html @@ -0,0 +1 @@ +{{ h.follow_button(type, id) }} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/base.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/base.html new file mode 100644 index 0000000..261da6e --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/base.html @@ -0,0 +1,130 @@ +{# Allows the DOCTYPE to be set on a page by page basis #} +{%- block doctype %}{% endblock -%} + +{# Allows custom attributes to be added to the tag #} +{%- block htmltag -%} +{% set lang = h.lang() %} + + +{%- endblock -%} + + {# Allows custom attributes to be added to the tag #} + + {# + Add custom meta tags to the page. Call super() to get the default tags + such as charset, viewport and generator. + + Example: + + {% block meta %} + {{ super() }} + + {% endblock %} + + #} + {%- block meta -%} + + + + + {% block meta_generator %}{% endblock %} + {% block meta_viewport %}{% endblock %} + {%- endblock -%} + + {# + Add a custom title to the page by extending the title block. Call super() + to get the default page title. + + Example: + + {% block title %}My Subtitle - {{ super() }}{% endblock %} + + #} + + {%- block title -%} + {%- block subtitle %}{% endblock -%} + {%- if self.subtitle()|trim %} {{ g.template_title_delimiter }} {% endif -%} + {{ g.site_title }} + {%- endblock -%} + + + {# + The links block allows you to add additonal content before the stylesheets + such as rss feeds and favicons in the same way as the meta block. + #} + {% block links -%} + + {% endblock -%} + + {# + The styles block allows you to add additonal stylesheets to the page in + the same way as the meta block. Use super() to include the default + stylesheets before or after your own. + + Example: + + {% block styles %} + {{ super() }} + + {% endblock %} + #} + {%- block styles %} + {# TODO: store just name of asset instead of path to it. #} + {% set theme = h.get_rtl_theme() if h.is_rtl_language() else g.theme %} + {% asset theme %} + {% asset 'd4science_theme/d4science-js' %} + {#{% asset 'd4science_theme/d4science-css' %}#} + {% endblock %} + + {% block head_extras %} + {# defined in the config.ini under "ckan.template_head_end" #} + {{ g.template_head_end | safe }} + {% endblock %} + + {# render all assets included in styles block #} + {{ h.render_assets('style') }} + {%- block custom_styles %} + {%- if g.site_custom_css -%} + + {%- endif %} + {% endblock %} + + + {# Allows custom attributes to be added to the tag #} + + + {# + The page block allows you to add content to the page. Most of the time it is + recommended that you extend one of the page.html templates in order to get + the site header and footer. If you need a clean page then this is the + block to use. + + Example: + + {% block page %} +
    Some other page content
    + {% endblock %} + #} + {%- block page %}{% endblock -%} + + {# + DO NOT USE THIS BLOCK FOR ADDING SCRIPTS + Scripts should be loaded by the {% assets %} tag except in very special + circumstances + #} + {%- block scripts %} + {% endblock -%} + + {% block body_extras -%} + {# defined in the config.ini under "ckan.template_footer_end" #} + {{ g.template_footer_end | safe }} + {%- endblock %} + + {# render all assets included in scripts block and everywhere else #} + {# make sure there are no calls to `asset` tag after this point #} + {{ h.render_assets('style') }} + {{ h.render_assets('script') }} + + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/dataviewer/base.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/dataviewer/base.html new file mode 100644 index 0000000..9f3d714 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/dataviewer/base.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block subtitle %}{{ h.dataset_display_name(package) }} {{ g.template_title_delimiter }} {{h.resource_display_name(resource) }}{% endblock %} + +{# remove any scripts #} +{% block scripts %} + +{% endblock %} + +{# remove any ckan styles #} +{% block styles %}{% endblock %} + +{% block custom_styles %}{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/primer.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/primer.html new file mode 100644 index 0000000..fe4ddc8 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/primer.html @@ -0,0 +1,100 @@ +{% extends "page.html" %} + +{% block toolbar %} +{% snippet 'development/snippets/breadcrumb.html', stage=1 %} +{% snippet 'development/snippets/breadcrumb.html', stage=2 %} +{% snippet 'development/snippets/breadcrumb.html', stage=3 %} +{% endblock %} + +{% block actions_content %} +{% snippet 'development/snippets/actions.html' %} +{% endblock %} + +{% block secondary_content %} +{% snippet 'development/snippets/context.html' %} + +
    +

    Helper text

    +
    {{ lipsum(1) }}
    +
    + +{% snippet 'development/snippets/nav.html', heading='Navigation' %} +{% snippet 'development/snippets/nav.html', heading='Active Navigation', show_active=true %} +{% snippet 'development/snippets/nav.html', heading='Icon Navigation', show_icons=true %} +{% snippet 'development/snippets/facet.html', heading='Facet List', show_icons=true %} +{% endblock %} + +{% block primary_content %} +{% snippet 'development/snippets/page_header.html' %} + +
    +
    + + + + +
    +
    + +
    +

    Top level heading (h1)

    +

    Some Rendered Markdown (h2)

    +
    +

    Heading 1

    + {{ lipsum(1) }} +

    Heading 2

    + {{ lipsum(1) }} +

    Heading 3

    + {{ lipsum(1) }} +
    +
    + +
    +
    +

    Forms

    +
    +{% snippet 'development/snippets/form.html' %} +{% snippet 'development/snippets/form.html', error=['This field has an error'] %} + +
    +
    +

    Form stages

    +
    +{% snippet 'development/snippets/form_stages.html' %} + + +
    +

    Datasets

    +
    + {% snippet 'snippets/package_list.html', packages=[ + {'name': "test", 'title': 'Dataset #1', 'type': 'dataset', 'notes': lipsum(1), 'tracking_summary':{'recent': 10}}, + {'name': "test", 'title': 'Dataset #2', 'type': 'dataset', 'notes': lipsum(0), 'tracking_summary':{'recent': 5}}, + {'name': "test", 'title': 'Dataset #3', 'type': 'dataset', 'notes': lipsum(1), 'tracking_summary':{'recent': 10}}, + {'name': "test", 'title': 'Dataset #4', 'type': 'dataset', 'notes': lipsum(1), 'tracking_summary':{'recent': 10}} + ] %} +
    + +
    +
    +

    Media Grid

    +
    +{% snippet 'development/snippets/media_grid.html', groups=[ +{'name': "test", 'display_name': 'Group #1', 'type': 'group', 'description': lipsum(0), 'packages': 0}, +{'name': "test", 'display_name': 'Group #2', 'type': 'group', 'description': lipsum(1), 'packages': 1}, +{'name': "test", 'display_name': 'Group #3', 'type': 'group', 'description': lipsum(1), 'packages': 10}, +{'name': "test", 'display_name': 'Group #4', 'type': 'group', 'description': lipsum(1), 'packages': 200}, +{'name': "test", 'display_name': 'Group #5', 'type': 'group', 'description': lipsum(1), 'packages': 10}, +{'name': "test", 'display_name': 'Group #6', 'type': 'group', 'description': lipsum(0), 'packages': 5} +] %} + +
    +

    Pagination

    +
    +{% snippet 'development/snippets/pagination.html', total=5, current=1 %} +{% snippet 'development/snippets/pagination.html', total=5, current=3 %} +{% snippet 'development/snippets/pagination.html', total=5, current=5 %} + +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/actions.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/actions.html new file mode 100644 index 0000000..fe9519c --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/actions.html @@ -0,0 +1,2 @@ +
  • Button
  • +
  • Primary Button
  • diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/breadcrumb.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/breadcrumb.html new file mode 100644 index 0000000..9a1e371 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/breadcrumb.html @@ -0,0 +1,7 @@ +
    + +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/context.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/context.html new file mode 100644 index 0000000..936d928 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/context.html @@ -0,0 +1,26 @@ +
    +
    +
    + + + +
    +

    {{ title }}

    +

    + {{ h.markdown_extract(lipsum(1), 160) }} +

    +

    + read more +

    +
    +
    +
    Stat #1
    +
    {{ h.SI_number_span(11111) }}
    +
    +
    +
    Stat #2
    +
    {{ h.SI_number_span(111) }}
    +
    +
    +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/facet.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/facet.html new file mode 100644 index 0000000..9f872cd --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/facet.html @@ -0,0 +1,15 @@ +
    + {% with items=(("First", true), ("Second", false), ("Third", true), ("Fourth", false), ("Last", false)) %} +

    Facet List

    + + + {% endwith %} +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/form.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/form.html new file mode 100644 index 0000000..800fa0b --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/form.html @@ -0,0 +1,27 @@ +{% import 'macros/form.html' as form %} + +
    +
    + {{ form.input('standard', label=_('Standard'), placeholder=_('Standard Input'), value='', error=error, classes=[]) }} + {{ form.input('standard', label=_('Medium'), placeholder=_('Medium Width Input'), value='', error=error, classes=['control-medium']) }} + {{ form.input('standard', label=_('Full'), placeholder=_('Full Width Input'), value='', error=error, classes=['control-full']) }} + {{ form.input('standard', label=_('Large'), placeholder=_('Large Input'), value='', error=error, classes=['control-full', 'control-large']) }} + {{ form.prepend('slug', label=_('Prepend'), prepend='prefix', placeholder=_('Prepend Input'), value='', error=error, classes=[]) }} + {{ form.custom( + names=('custom_key', 'custom_value', 'custom_deleted'), + id='field-custom', + label=_('Custom Field (empty)'), + values=(), + error=error ) }} + {{ form.custom( + names=('custom_key', 'custom_value', 'custom_deleted'), + id='field-custom', + label=_('Custom Field'), + values=('key', 'value', true), + error=error ) }} + {{ form.markdown('desc', id='field-description', label=_('Markdown'), placeholder='Some nice placeholder text', error=error) }} + {{ form.textarea('desc', id='field-description', label=_('Textarea'), placeholder='Some nice placeholder text', error=error) }} + {{ form.select('year', label=_('Select'), options=[{'value': 2010}, {'value': 2011}], selected=2011, error=error) }} + {{ form.checkbox('remember', label="This is my checkbox", checked=true, error=error) }} +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/form_stages.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/form_stages.html new file mode 100644 index 0000000..409f0f5 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/form_stages.html @@ -0,0 +1,30 @@ +
    +
    + {% snippet 'package/snippets/stages.html', stages=['active'] %} +
    +
    +
    +
    + {% snippet 'package/snippets/stages.html', stages=['complete', 'active'] %} +
    +
    +
    +
    + {% snippet 'package/snippets/stages.html', stages=['complete', 'complete', 'active'] %} +
    +
    +
    +
    + {% snippet 'package/snippets/stages.html', stages=['complete', 'active', 'complete'] %} +
    +
    +
    +
    + {% snippet 'package/snippets/stages.html', stages=['active', 'complete', 'complete'] %} +
    +
    +
    +
    + {% snippet 'package/snippets/stages.html', stages=['active', 'complete'] %} +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/list.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/list.html new file mode 100644 index 0000000..48dbd87 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/list.html @@ -0,0 +1,14 @@ +
    + {% with items=(("First", false), ("Second", true), ("Third", true), ("Fourth", false), ("Last", false)) %} +

    {{ heading }}

    + + {% endwith %} +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/media_grid.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/media_grid.html new file mode 100644 index 0000000..0ef561f --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/media_grid.html @@ -0,0 +1,5 @@ +
    +
    + {% snippet 'group/snippets/group_list.html', groups=groups %} +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/module.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/module.html new file mode 100644 index 0000000..bc9c8e1 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/module.html @@ -0,0 +1,21 @@ +{% with classes = classes or [], hn = heading_level or 1 %} +
    + {% if heading_link %} + {{ heading }} + {% elif heading_action %} + {{ heading }} + {% elif heading_icon %} + {{ heading }} + {% else %} + {{ heading }} + {% endif %} +
    + {{ lipsum(1) }} +
    + {% if footer %} + + {% endif %} +
    +{% endwith %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/nav.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/nav.html new file mode 100644 index 0000000..da5d917 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/nav.html @@ -0,0 +1,14 @@ +
    + {% with items=(("First", true), ("Second", false), ("Third", true), ("Fourth", false), ("Last", false)) %} +

    {{ heading }}

    + + {% endwith %} +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/page_header.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/page_header.html new file mode 100644 index 0000000..cfc49c9 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/page_header.html @@ -0,0 +1,11 @@ +{% with items=(("First", true), ("Second", false), ("Third", false)) %} + +{% endwith %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/pagination.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/pagination.html new file mode 100644 index 0000000..1a375f0 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/pagination.html @@ -0,0 +1,11 @@ +
    +
    +
      + {% if current != 1 %}
    • «
    • {% endif %} + {% for index in range(1, total+1) %} + {{ index }} + {% endfor %} + {% if current != total %}
    • »
    • {% endif %} +
    +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/simple-input.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/simple-input.html new file mode 100644 index 0000000..00d4d25 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/development/snippets/simple-input.html @@ -0,0 +1,4 @@ +
    +

    Module Narrow Input

    +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/invite_user.txt b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/invite_user.txt new file mode 100644 index 0000000..d9deffe --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/invite_user.txt @@ -0,0 +1,19 @@ +{% trans %} +Dear {{ user_name }}, + +You have been invited to {{ site_title }}. + +A user has already been created for you with the username {{ user_name }}. You can change it later. + +You have been added to the {{ group_type }} {{ group_title }} with the following role: {{ role_name }}. + +To accept this invite, please reset your password at: + + {{ reset_link }} + + +Have a nice day. + +-- +Message sent by {{ site_title }} ({{ site_url }}) +{% endtrans %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/invite_user_subject.txt b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/invite_user_subject.txt new file mode 100644 index 0000000..1e992c1 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/invite_user_subject.txt @@ -0,0 +1,3 @@ +{% trans %} +Invite for {{ site_title }} +{% endtrans %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/reset_password.txt b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/reset_password.txt new file mode 100644 index 0000000..19936fa --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/reset_password.txt @@ -0,0 +1,14 @@ +{% trans %} +Dear {{ user_name }}, + +You have requested your password on {{ site_title }} to be reset. + +Please click the following link to confirm this request: + + {{ reset_link }} + +Have a nice day. + +-- +Message sent by {{ site_title }} ({{ site_url }}) +{% endtrans %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/reset_password_subject.txt b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/reset_password_subject.txt new file mode 100644 index 0000000..4a8941c --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/emails/reset_password_subject.txt @@ -0,0 +1,3 @@ +{% trans %} +Reset your password - {{ site_title }} +{% endtrans %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/error_document_template.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/error_document_template.html new file mode 100644 index 0000000..51acb19 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/error_document_template.html @@ -0,0 +1,31 @@ +{% extends "page.html" %} + +{% block subtitle %}{{ gettext('Error %(error_code)s', error_code=code) }}{% endblock %} + +{% block primary %} +
    +
    + {% if name %} +

    {{ code }} {{ name }}

    + {% endif %} + {{ content}} +
    + {% block login_redirect %} + {% if show_login_redirect_link %} +
    + {{ _("You might need to login to access this page.") }} {{ _("Click here to login") }} +
    + {% endif %} + {% endblock %} +
    +{% endblock %} + +{% block breadcrumb %} +{% endblock %} + +{% block flash %} + {# eat the flash messages caused by the 404 #} + {% set flash_messages = h.get_flashed_messages() %} +{% endblock %} + +{% block secondary %}{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/footer.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/footer.html new file mode 100644 index 0000000..f149c21 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/footer.html @@ -0,0 +1,20 @@ +{% block header_hide_container_content %} +{% set hideParameter = h.get_request_param('hh', None) %} +{% set hide = h.get_cookie_value('ckan_hide_header') %} +
    +{% if hide=='true' %} +{# Updated and unified the footer, see #18126 #} +
    +gCube Catalogue powered by CKAN + |  +Access this catalogue using the gCat APIs +
    +{% else %} +
    +gCube Catalogue powered by CKAN + |  +Access this catalogue using the gCat APIs +
    +{% endif %} +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/about.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/about.html new file mode 100644 index 0000000..07461cf --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/about.html @@ -0,0 +1,16 @@ +{% extends "group/read_base.html" %} + +{% block subtitle %}{{ _('About') }} {{ g.template_title_delimiter }} {{ super() }}{% endblock %} + +{% block primary_content_inner %} +

    {% block page_heading %}{{ group_dict.display_name }}{% endblock %}

    + {% block group_description %} + {% if group_dict.description %} + {{ h.render_markdown(group_dict.description) }} + {% endif %} + {% endblock %} + + {% block group_extras %} + {% snippet 'snippets/additional_info.html', extras = h.sorted_extras(group_dict.extras) %} + {% endblock %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/admins.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/admins.html new file mode 100644 index 0000000..ee98a8a --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/admins.html @@ -0,0 +1,10 @@ +{% extends "group/read_base.html" %} + +{% block subtitle %}{{ _('Administrators') }} {{ g.template_title_delimiter }} {{ group_dict.title or group_dict.name }}{% endblock %} + +{% block primary_content_inner %} +

    {% block page_heading %}{{ _('Administrators') }}{% endblock %}

    + {% block admins_list %} + {% snippet "user/snippets/followers.html", followers=admins %} + {% endblock %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/base_form_page.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/base_form_page.html new file mode 100644 index 0000000..5dea2ef --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/base_form_page.html @@ -0,0 +1,13 @@ +{% extends "group/edit_base.html" %} + + +{% block primary_content_inner %} +

    + {% block page_heading %} + {{ h.humanize_entity_type('group', group_type, 'form label') or _('Group Form'), }} + {% endblock %} +

    + {% block form %} + {{ form | safe }} + {% endblock %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/confirm_delete.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/confirm_delete.html new file mode 100644 index 0000000..8c4b2e6 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/confirm_delete.html @@ -0,0 +1,22 @@ +{% extends "page.html" %} + +{% block subtitle %}{{ _("Confirm Delete") }}{% endblock %} + +{% block maintag %}
    {% endblock %} + +{% block main_content %} +
    +
    + {% block form %} +

    {{ _('Are you sure you want to delete group - {name}?').format(name=group_dict.name) }}

    +

    +

    + {{ h.csrf_input() }} + + +
    +

    + {% endblock %} +
    +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/confirm_delete_member.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/confirm_delete_member.html new file mode 100644 index 0000000..4f31781 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/confirm_delete_member.html @@ -0,0 +1,23 @@ +{% extends "page.html" %} + +{% block subtitle %}{{ _("Confirm Delete") }}{% endblock %} + +{% block maintag %}
    {% endblock %} + +{% block main_content %} +
    +
    + {% block form %} +

    {{ _('Are you sure you want to delete member - {name}?').format(name=user_dict.name) }}

    +

    +

    + {{ h.csrf_input() }} + + + +
    +

    + {% endblock %} +
    +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/edit.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/edit.html new file mode 100644 index 0000000..31dcd29 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/edit.html @@ -0,0 +1,12 @@ +{% extends "group/base_form_page.html" %} + +{% block breadcrumb_content %} +
  • {% link_for h.humanize_entity_type('group', group_type, 'breadcrumb') or _('Groups'), named_route=group_type+'.index' %}
  • + {% block breadcrumb_content_inner %} +
  • {% link_for group.display_name|truncate(35), named_route=group_type+'.read', id=group.name, title=group.display_name %}
  • +
  • {% link_for _('Manage'), named_route=group_type+'.edit', id=group.name %}
  • + {% endblock %} +{% endblock %} + +{% block page_heading_class %}hide-heading{% endblock %} +{% block page_heading %}{{ h.humanize_entity_type('group', group_type, 'edit label') or _('Edit Group') }}{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/edit_base.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/edit_base.html new file mode 100644 index 0000000..ec92493 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/edit_base.html @@ -0,0 +1,19 @@ +{% extends "page.html" %} +{% set dataset_type = h.default_package_type() %} + +{% block subtitle %}{{ _('Manage') }} {{ g.template_title_delimiter }} {{ group_dict.display_name }} {{ g.template_title_delimiter }} {{ h.humanize_entity_type('group', group_type, 'page title') or _('Groups') }}{% endblock %} + +{% set group = group_dict %} + +{% block content_action %} + {% link_for _('View'), named_route=group_type+'.read', id=group_dict.name, class_='btn btn-default', icon='eye' %} +{% endblock %} + +{% block content_primary_nav %} + {{ h.build_nav_icon(group_type + '.edit', _('Edit'), id=group_dict.name, icon='pencil-square') }} + {{ h.build_nav_icon(group_type + '.members', _('Members'), id=group_dict.name, icon='users') }} +{% endblock %} + +{% block secondary_content %} + {% snippet "group/snippets/info.html", group=group_dict, show_nums=false %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/followers.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/followers.html new file mode 100644 index 0000000..c51255c --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/followers.html @@ -0,0 +1,10 @@ +{% extends "group/read_base.html" %} + +{% block subtitle %}{{ _('Followers') }} {{ g.template_title_delimiter }} {{ group_dict.title or group_dict.name }}{% endblock %} + +{% block primary_content_inner %} +

    {% block page_heading %}{{ _('Followers') }}{% endblock %}

    + {% block followers_list %} + {% snippet "user/snippets/followers.html", followers=followers %} + {% endblock %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/index.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/index.html new file mode 100644 index 0000000..1501bc8 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/index.html @@ -0,0 +1,44 @@ +{% extends "page.html" %} + + +{% block subtitle %}{{ h.humanize_entity_type('group', group_type, 'page title') or _('Groups') }}{% endblock %} + +{% block breadcrumb_content %} +
  • {% link_for h.humanize_entity_type('group', group_type, 'breadcrumb') or _('Groups'), named_route=group_type+'.index' %}
  • +{% endblock %} + +{% block page_header %}{% endblock %} + +{% block page_primary_action %} + {% if h.check_access('group_create') %} + {% link_for h.humanize_entity_type('group', group_type, 'add link') or _('Add Group'), named_route=group_type+'.new', class_='btn btn-primary', icon='plus-square' %} + {% endif %} +{% endblock %} + +{% block primary_content_inner %} +

    {{ h.humanize_entity_type('group', group_type, 'page title') or _('Groups') }}

    + {% block groups_search_form %} + {% snippet 'snippets/search_form.html', form_id='group-search-form', type=group_type, query=q, sorting_selected=sort_by_selected, count=page.item_count, placeholder=h.humanize_entity_type('group', group_type, 'search placeholder') or _('Search groups...'), show_empty=request.args, no_bottom_border=true if page.items, sorting = [(_('Name Ascending'), 'title asc'), (_('Name Descending'), 'title desc')] %} + {% endblock %} + {% block groups_list %} + {% if page.items or request.args %} + {% if page.items %} + {% snippet "group/snippets/group_list.html", groups=page.items %} + {% endif %} + {% else %} +

    + {{ h.humanize_entity_type('group', group_type, 'no any objects') or _('There are currently no groups for this site') }}. + {% if h.check_access('group_create') %} + {% link_for _('How about creating one?'), named_route=group_type+'.new' %}. + {% endif %} +

    + {% endif %} + {% endblock %} + {% block page_pagination %} + {{ page.pager(q=q or '', sort=sort_by_selected or '') }} + {% endblock %} +{% endblock %} + +{% block secondary_content %} + {% snippet "group/snippets/helper.html", group_type=group_type %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/member_new.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/member_new.html new file mode 100644 index 0000000..3234f71 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/member_new.html @@ -0,0 +1,101 @@ +{% extends "group/edit_base.html" %} + +{% import 'macros/form.html' as form %} + +{% set user = user_dict %} + +{% block primary_content_inner %} + {% link_for _('Back to all members'), named_route=group_type+'.members', id=group.name, class_='btn btn-default pull-right', icon='arrow-left' %} +

    + {% block page_heading %}{{ _('Edit Member') if user else _('Add Member') }}{% endblock %} +

    + {% block form %} +
    + {{ h.csrf_input() }} +
    +
    +
    + {% if not user %} + +

    + {{ _('If you wish to add an existing user, search for their username below.') }} +

    + {% endif %} +
    + {% if user %} + + + {% else %} + + {% endif %} +
    +
    +
    + {% if not user %} +
    +
    + {{ _('or') }} +
    +
    +
    +
    + +

    + {{ _('If you wish to invite a new user, enter their email address.') }} +

    +
    + +
    +
    +
    + {% endif %} +
    + + {% if user and user.name == c.user and user_role == 'admin' %} + {% set format_attrs = {'data-module': 'autocomplete', 'disabled': 'disabled'} %} + {{ form.select('role', label=_('Role'), options=roles, selected=user_role, error='', attrs=format_attrs) }} + {{ form.hidden('role', value=user_role) }} + {% else %} + {% set format_attrs = {'data-module': 'autocomplete'} %} + {{ form.select('role', label=_('Role'), options=roles, selected=user_role, error='', attrs=format_attrs) }} + {% endif %} + +
    + {% if user %} + {{ _('Delete') }} + + {% else %} + + {% endif %} +
    +
    + {% endblock %} +{% endblock %} + +{% block secondary_content %} + {{ super() }} +
    +

    + + {{ _('What are roles?') }} +

    +
    + {% trans %} +

    Admin: Can edit group information, as well as + manage organization members.

    +

    Member: Can add/remove datasets from groups

    + {% endtrans %} +
    +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/members.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/members.html new file mode 100644 index 0000000..31d09ef --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/members.html @@ -0,0 +1,38 @@ +{% extends "group/edit_base.html" %} + +{% block subtitle %}{{ _('Members') }} {{ g.template_title_delimiter }} {{ group_dict.display_name }} {{ g.template_title_delimiter }} {{ h.humanize_entity_type('group', group_type, 'page title') or _('Groups') }}{% endblock %} + +{% block page_primary_action %} + {% link_for _('Add Member'), named_route=group_type+'.member_new', id=group_dict.id, class_='btn btn-primary', icon='plus-square' %} +{% endblock %} + +{% block primary_content_inner %} +

    {{ _('{0} members'.format(members|length)) }}

    + + + + + + + + + + {% for user_id, user, role in members %} + + + + + + {% endfor %} + +
    {{ _('User') }}{{ _('Role') }}
    + {{ h.linked_user(user_id, maxlength=20) }} + {{ role }} + +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/new.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/new.html new file mode 100644 index 0000000..69230a3 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/new.html @@ -0,0 +1,25 @@ +{% extends "group/base_form_page.html" %} + +{% set label = h.humanize_entity_type('group', group_type, 'create title') or _('Create a Group') %} + + +{% block subtitle %}{{ label }}{% endblock %} + +{% block page_heading %}{{ label }}{% endblock %} + +{% block breadcrumb_content %} +
  • {{ h.nav_link( + h.humanize_entity_type('group', group_type, 'breadcrumb') or _('Groups'), + named_route=group_type+'.index') }}
  • +
  • + {{ h.nav_link( + label, + named_route=group_type~'.new') }} +
  • +{% endblock %} + +{% block page_header %}{% endblock %} + +{% block secondary_content %} + {% snippet "group/snippets/helper.html", group_type=group_type %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/new_group_form.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/new_group_form.html new file mode 100644 index 0000000..7fb4ccd --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/new_group_form.html @@ -0,0 +1,25 @@ +{% extends "group/snippets/group_form.html" %} + +{# + As the form is rendered as a seperate page we take advantage of this by + overriding the form blocks depending on the current context + #} +{% block dataset_fields %} + {% if action == "edit" %}{{ super() }}{% endif %} +{% endblock %} + +{% block custom_fields %} + {% if action == "edit" %}{{ super() }}{% endif %} +{% endblock %} + +{% block save_text %} + {%- if action == "edit" -%} + {{ h.humanize_entity_type('group', group_type, 'update label') or _('Update Group') }} + {%- else -%} + {{ h.humanize_entity_type('group', group_type, 'create label') or _('Create Group') }} + {%- endif -%} +{% endblock %} + +{% block delete_button %} + {% if action == "edit" %}{{ super() }}{% endif %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/read.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/read.html new file mode 100644 index 0000000..ff69aa7 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/read.html @@ -0,0 +1,42 @@ +{% extends "group/read_base.html" %} +{% set dataset_type = h.default_package_type() %} + +{% block primary_content_inner %} + {% block groups_search_form %} + {% set facets = { + 'fields': fields_grouped, + 'search': search_facets, + 'titles': facet_titles, + 'translated_fields': translated_fields, + 'remove_field': remove_field } + %} + {% set sorting = [ + (_('Relevance'), 'score desc, metadata_modified desc'), + (_('Name Ascending'), 'title_string asc'), + (_('Name Descending'), 'title_string desc'), + (_('Last Modified'), 'metadata_modified desc'), + (_('Popular'), 'views_recent desc') if g.tracking_enabled else (false, false) ] + %} + {% snippet 'snippets/search_form.html', form_id='group-datasets-search-form', type=dataset_type, query=q, sorting=sorting, sorting_selected=sort_by_selected, count=page.item_count, facets=facets, placeholder=h.humanize_entity_type('package', dataset_type, 'search placeholder') or _('Search datasets...'), show_empty=request.args, fields=fields %} + {% endblock %} + {% block packages_list %} + {% if page.items %} + {{ h.snippet('snippets/package_list.html', packages=page.items) }} + {% endif %} + {% endblock %} + {% block page_pagination %} + {{ page.pager(q=q) }} + {% endblock %} +{% endblock %} + +{% block secondary_content %} + {{ super() }} +
    +
    + {% for facet in facet_titles %} + {{ h.snippet('snippets/facet_list.html', title=facet_titles[facet], name=facet, extras={'id':group_dict.id}, search_facets=search_facets) }} + {% endfor %} +
    + close +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/read_base.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/read_base.html new file mode 100644 index 0000000..75c869b --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/read_base.html @@ -0,0 +1,29 @@ +{% extends "page.html" %} +{% set dataset_type = h.default_package_type() %} + +{% block subtitle %}{{ group_dict.display_name }} {{ g.template_title_delimiter }} {{ h.humanize_entity_type('group', group_type, 'page title') or _('Groups') }}{% endblock %} + +{% block breadcrumb_content %} +
  • {% link_for h.humanize_entity_type('group', group_type, 'breadcrumb') or _('Groups'), named_route=group_type+'.index' %}
  • +
  • {% link_for group_dict.display_name|truncate(35), named_route=group_type+'.read', id=group_dict.name, title=group_dict.display_name %}
  • +{% endblock %} + +{% block content_action %} + {% if h.check_access('group_update', {'id': group_dict.id}) %} + {% link_for _('Manage'), named_route=group_type+'.edit', id=group_dict.name, class_='btn btn-default', icon='wrench' %} + {% endif %} +{% endblock %} + +{% block content_primary_nav %} + {{ h.build_nav_icon(group_type + '.read', h.humanize_entity_type('package', dataset_type, 'content tab') or _('Datasets'), id=group_dict.name, icon='sitemap') }} + {{ h.build_nav_icon(group_type + '.about', _('About'), id=group_dict.name, icon='info-circle') }} +{% endblock %} + +{% block secondary_content %} + {% snippet "group/snippets/info.html", group=group_dict, show_nums=true %} +{% endblock %} + +{% block links %} + {{ super() }} + {% include "group/snippets/feeds.html" %} +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/feeds.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/feeds.html new file mode 100644 index 0000000..26de3a6 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/feeds.html @@ -0,0 +1,2 @@ +{%- set dataset_feed = h.url_for('feeds.group', id=group_dict.name) -%} + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/group_form.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/group_form.html new file mode 100644 index 0000000..2e752b5 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/group_form.html @@ -0,0 +1,45 @@ +{% import 'macros/form.html' as form %} + + +
    + {{ h.csrf_input() }} + {% block error_summary %} + {{ form.errors(error_summary) }} + {% endblock %} + + {% block basic_fields %} + {% set attrs = {'data-module': 'slug-preview-target', 'class': 'form-control'} %} + {{ form.input('title', label=_('Name'), id='field-name', placeholder=h.humanize_entity_type('group', group_type, 'name placeholder') or _('My Group'), value=data.title, error=errors.title, classes=['control-full'], attrs=attrs) }} + + {# Perhaps these should be moved into the controller? #} + {% set prefix = h.url_for(group_type + '.read', id='') %} + {% set domain = h.url_for(group_type + '.read', id='', qualified=true) %} + {% set domain = domain|replace("http://", "")|replace("https://", "") %} + {% set attrs = {'data-module': 'slug-preview-slug', 'class': 'form-control input-sm', 'data-module-prefix': domain, 'data-module-placeholder': '<' + group_type + '>'} %} + + {{ form.prepend('name', label=_('URL'), prepend=prefix, id='field-url', placeholder=_('my-' + group_type), value=data.name, error=errors.name, attrs=attrs, is_required=true) }} + + {{ form.markdown('description', label=_('Description'), id='field-description', placeholder=h.humanize_entity_type('group', group_type, 'description placeholder') or _('A little information about my group...'), value=data.description, error=errors.description) }} + + {% set is_upload = data.image_url and not data.image_url.startswith('http') %} + {% set is_url = data.image_url and data.image_url.startswith('http') %} + + {{ form.image_upload(data, errors, is_upload_enabled=h.uploads_enabled(), is_url=is_url, is_upload=is_upload) }} + + {% endblock %} + + {% block custom_fields %} + {% snippet 'snippets/custom_form_fields.html', extras=data.extras, errors=errors, limit=3 %} + {% endblock %} + + {{ form.required_message() }} + +
    + {% block delete_button %} + {% if h.check_access('group_delete', {'id': data.id}) %} + {% block delete_button_text %}{{ _('Delete') }}{% endblock %} + {% endif %} + {% endblock %} + +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/group_item.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/group_item.html new file mode 100644 index 0000000..1908ca6 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/group_item.html @@ -0,0 +1,50 @@ +{# +Renders a media item for a group. This should be used in a list. + +group - A group dict. + +Example: + +
      + {% for group in groups %} + {% snippet "group/snippets/group_item.html", group=group %} + {% endfor %} +
    +#} +{% set type = group.type or 'group' %} +{% set url = h.url_for(type ~ '.read', id=group.name) %} +{% block item %} +
  • + {% block item_inner %} + {% block image %} + {{ group.name }} + {% endblock %} + {% block title %} +

    {{ group.display_name }}

    + {% endblock %} + {% block description %} + {% if group.description %} +

    {{ h.markdown_extract(group.description, extract_length=80) }}

    + {% endif %} + {% endblock %} + {% block datasets %} + {% if group.package_count %} + {{ ungettext('{num} Dataset', '{num} Datasets', group.package_count).format(num=group.package_count) }} + {% elif group.package_count == 0 %} + {{ _('0 Datasets') }} + {% endif %} + {% endblock %} + {% block link %} + + {{ _('View {name}').format(name=group.display_name) }} + + {% endblock %} + {% if group.user_member %} + + {% endif %} + {% endblock %} +
  • +{% endblock %} +{% if position is divisibleby 3 %} +
  • +{% endif %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/group_list.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/group_list.html new file mode 100644 index 0000000..d171296 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/group_list.html @@ -0,0 +1,19 @@ +{# +Display a grid of group items. + +groups - A list of groups. + +Example: + + {% snippet "group/snippets/group_list.html" %} + +#} +{% block group_list %} +
      + {% block group_list_inner %} + {% for group in groups %} + {% snippet "group/snippets/group_item.html", group=group, position=loop.index %} + {% endfor %} + {% endblock %} +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/helper.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/helper.html new file mode 100644 index 0000000..7061646 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/helper.html @@ -0,0 +1,27 @@ +{# + Displays a sidebard module with information about group. + + group_type - The type of group. + + Example: + + {% snippet "group/snippets/helper.html", group_type=group_type %} + + #} + +
    +

    + + {{ _('What are Groups?') }} +

    +
    +

    + {% trans %} + You can use CKAN Groups to create and manage collections of datasets. + This could be to catalogue datasets for a particular project or team, + or on a particular theme, or as a very simple way to help people find + and search your own published datasets. + {% endtrans %} +

    +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/info.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/info.html new file mode 100644 index 0000000..5916124 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/group/snippets/info.html @@ -0,0 +1,54 @@ +{% set dataset_type = h.default_package_type() %} + +{% block info %} +
    +
    + {% block inner %} + {% block image %} +
    + + {{ group.name }} + +
    + {% endblock %} + {% block heading %} +

    + {{ group.display_name }} + {% if group.state == 'deleted' %} + [{{ _('Deleted') }}] + {% endif %} +

    + {% endblock %} + {% block description %} + {% if group.description %} +

    + {{ h.markdown_extract(group.description, 180) }} +

    +

    + {% link_for _('read more'), named_route='group.about', id=group.name %} +

    + {% endif %} + {% endblock %} + {% if show_nums %} + {% block nums %} +
    +
    +
    {{ _('Followers') }}
    +
    {{ h.SI_number_span(group.num_followers) }}
    +
    +
    +
    {{ h.humanize_entity_type('package', dataset_type, 'facet label') or _('Datasets') }}
    +
    {{ h.SI_number_span(group.package_count) }}
    +
    +
    + {% endblock %} + {% block follow %} + + {% endblock %} + {% endif %} + {% endblock %} +
    +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/header.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/header.html new file mode 100644 index 0000000..b3b1f17 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/header.html @@ -0,0 +1,123 @@ +{% set dataset_type = h.default_package_type() %} + +{% block header_wrapper %} {% block header_account %} + +{% endblock %} +
    +
    + {% block header_debug %} {% if g.debug and not g.debug_supress_header %} +
    Blueprint : {{ g.blueprint }}
    View : {{ g.view }}
    + {% endif %} {% endblock %} + +
    +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/about.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/about.html new file mode 100644 index 0000000..8ed25d8 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/about.html @@ -0,0 +1,24 @@ +{% extends "page.html" %} + +{% block subtitle %}{{ _('About') }}{% endblock %} + +{% block breadcrumb_content %} +
  • {% link_for _('About'), named_route='home.about' %}
  • +{% endblock %} + +{% block primary %} +
    +
    + {% block about %} + {% if g.site_about %} + {{ h.render_markdown(g.site_about) }} + {% else %} +

    {{ _('About') }}

    + {% snippet 'home/snippets/about_text.html' %} + {% endif %} + {% endblock %} +
    +
    +{% endblock %} + +{% block secondary %}{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/index.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/index.html new file mode 100644 index 0000000..c14d4e0 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/index.html @@ -0,0 +1,18 @@ +{% extends "page.html" %} +{% set homepage_style = ( g.homepage_style or '1' ) %} + +{% block subtitle %}{{ _("Welcome") }}{% endblock %} + +{% block maintag %}{% endblock %} +{% block toolbar %}{% endblock %} + +{% block content %} +
    +
    + {{ self.flash() }} +
    + {% block primary_content %} + {% snippet "home/layout{0}.html".format(homepage_style), search_facets=search_facets %} + {% endblock %} +
    +{% endblock %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/layout1.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/layout1.html new file mode 100644 index 0000000..60c129d --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/layout1.html @@ -0,0 +1,40 @@ +
    +
    +
    +
    +
    + {% block promoted %} + {% snippet 'home/snippets/promoted.html' %} + {% endblock %} +
    +
    + {% block search %} + {% snippet 'home/snippets/search.html', search_facets=search_facets %} + {% endblock %} +
    +
    +
    +
    + +
    +
    +
    +
    + {# Note: this featured_group block is used as an example in the theming + tutorial in the docs! If you change this code, be sure to check + whether you need to update the docs. #} + {# Start template block example. #} + {% block featured_group %} + {% snippet 'home/snippets/featured_group.html' %} + {% endblock %} + {# End template block example. #} +
    +
    + {% block featured_organization %} + {% snippet 'home/snippets/featured_organization.html' %} + {% endblock %} +
    +
    +
    +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/layout2.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/layout2.html new file mode 100644 index 0000000..68beb5d --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/layout2.html @@ -0,0 +1,35 @@ +
    +
    +
    +
    + {% block search %} + {% snippet 'home/snippets/search.html', search_facets=search_facets %} + {% endblock %} + {% block stats %} + {% snippet 'home/snippets/stats.html' %} + {% endblock %} +
    +
    + {% block promoted %} + {% snippet 'home/snippets/promoted.html' %} + {% endblock %} +
    +
    +
    +
    +
    +
    +
    +
    + {% block featured_organization %} + {% snippet 'home/snippets/featured_organization.html' %} + {% endblock %} +
    +
    + {% block featured_group %} + {% snippet 'home/snippets/featured_group.html' %} + {% endblock %} +
    +
    +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/layout3.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/layout3.html new file mode 100644 index 0000000..fc9d98b --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/layout3.html @@ -0,0 +1,23 @@ +
    +
    + {% block search %} + {% snippet 'home/snippets/search.html', search_facets=search_facets %} + {% endblock %} +
    +
    +
    +
    +
    +
    + {% block promoted %} + {% snippet 'home/snippets/promoted.html' %} + {% endblock %} +
    +
    + {% block stats %} + {% snippet 'home/snippets/stats.html' %} + {% endblock %} +
    +
    +
    +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/robots.txt b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/robots.txt new file mode 100644 index 0000000..ca60362 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/robots.txt @@ -0,0 +1,12 @@ +User-agent: * +{% block all_user_agents -%} +Disallow: /dataset/rate/ +Disallow: /revision/ +Disallow: /dataset/*/history +Disallow: /api/ +Crawl-Delay: 10 +{%- endblock %} + +{% block additional_user_agents -%} +{%- endblock %} + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/about_text.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/about_text.html new file mode 100644 index 0000000..21deb89 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/about_text.html @@ -0,0 +1,20 @@ +{% trans %} +

    CKAN is the world’s leading open-source data portal platform.

    + +

    CKAN is a complete out-of-the-box software solution that makes data +accessible and usable – by providing tools to streamline publishing, sharing, +finding and using data (including storage of data and provision of robust data +APIs). CKAN is aimed at data publishers (national and regional governments, +companies and organizations) wanting to make their data open and available.

    + +

    CKAN is used by governments and user groups worldwide and powers a variety +of official and community data portals including portals for local, national +and international government, such as the UK’s data.gov.uk and the +United States catalog.data.gov, the Brazilian dados.gov.br, Dutch and +Netherland government portals, as well as city and municipal sites in the US, +UK, Argentina, Finland and elsewhere.

    + +

    CKAN: https://ckan.org/
    +CKAN Showcases: https://ckan.org/showcase
    +Features overview: https://ckan.org/features/

    +{% endtrans %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/featured_group.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/featured_group.html new file mode 100644 index 0000000..f411c1e --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/featured_group.html @@ -0,0 +1,7 @@ +{% set groups = h.get_featured_groups() %} + +{% for group in groups %} +
    + {% snippet 'snippets/group_item.html', group=group %} +
    +{% endfor %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/featured_organization.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/featured_organization.html new file mode 100644 index 0000000..d13d18e --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/featured_organization.html @@ -0,0 +1,7 @@ +{% set organizations = h.get_featured_organizations() %} + +{% for organization in organizations %} +
    + {% snippet 'snippets/organization_item.html', organization=organization %} +
    +{% endfor %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/promoted.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/promoted.html new file mode 100644 index 0000000..9503571 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/promoted.html @@ -0,0 +1,27 @@ +{% set intro = g.site_intro_text %} + +
    +
    + {% if intro %} + {{ h.render_markdown(intro) }} + {% else %} +

    {{ _("Welcome to CKAN") }}

    +

    + {% trans %}This is a nice introductory paragraph about CKAN or the site + in general. We don't have any copy to go here yet but soon we will + {% endtrans %} +

    + {% endif %} +
    + + {% block home_image %} + + {% endblock %} +
    diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/search.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/search.html new file mode 100644 index 0000000..89efb0c --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/search.html @@ -0,0 +1,22 @@ +{% set tags = h.get_facet_items_dict('tags', search_facets, limit=3) %} +{% set placeholder = _('E.g. environment') %} +{% set dataset_type = h.default_package_type() %} + + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/stats.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/stats.html new file mode 100644 index 0000000..c70d596 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/home/snippets/stats.html @@ -0,0 +1,29 @@ +{% set stats = h.get_site_statistics() %} + + diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/autoform.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/autoform.html new file mode 100644 index 0000000..c469b81 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/autoform.html @@ -0,0 +1,70 @@ +{# +Builds a form from the supplied form_info list/tuple. All form info dicts +can also contain an "extra_info" key which will add some help text after the +input element. + +form_info - A list of dicts describing the form field to build. +data - The form data object. +errors - The form errors object. +error_summary - A list of errors to display above the fields. + +Example + + {% set form_info = [ + {'name': 'ckan.site_title', 'control': 'input', 'label': _('Site Title'), 'placeholder': ''}, + {'name': 'ckan.theme', 'control': 'select', 'options': styles, 'label': _('Style'), 'placeholder': ''}, + {'name': 'ckan.site_description', 'control': 'input', 'label': _('Site Tag Line'), 'placeholder': ''}, + {'name': 'ckan.site_logo', 'control': 'input', 'label': _('Site Tag Logo'), 'placeholder': ''}, + {'name': 'ckan.site_about', 'control': 'markdown', 'label': _('About'), 'placeholder': _('About page text')}, + {'name': 'ckan.site_intro_text', 'control': 'markdown', 'label': _('Intro Text'), 'placeholder': _('Text on home page')}, + {'name': 'ckan.site_custom_css', 'control': 'textarea', 'label': _('Custom CSS'), 'placeholder': _('Customisable css inserted into the page header')}, + ] %} + + {% import 'macros/autoform.html' as autoform %} + {{ autoform.generate(form_info, data, errors) }} + +#} +{% import 'macros/form.html' as form %} +{%- macro generate(form_info=[], data={}, errors={}, error_summary=[]) -%} + {{ form.errors(error_summary) if error_summary }} + + {% for item in form_info %} + {% set name = item.name %} + {% set value = data.get(name) %} + {% set error = errors.get(name) %} + {% set id = 'field-%s' % (name|lower|replace('_', '-')|replace('.', '-')) %} + + {% set control = item.control or 'input' %} + {% set label = item.label %} + {% set placeholder = item.placeholder %} + + {% set classes = item.classes or [] %} + {% set classes = ['control-medium'] if not classes and control == 'input' %} + + {% if control == 'select' %} + {% call form.select(name, id=id, label=label, options=item.options, selected=value, error=error) %} + {% if item.extra_info %}{{ form.info(item.extra_info) }}{% endif %} + {% endcall %} + {% elif control == 'html' %} +
    +
    + {{ item.html }} +
    +
    + {% elif control == 'image_upload' %} + {% set field_url = item.field_url or 'image_url' %} + {% set is_upload = data[field_url] and not data[field_url].startswith('http') %} + {% set is_url = data[field_url] and data[field_url].startswith('http') %} + + {% set field_upload = item.field_upload or 'image_upload' %} + {% set field_clear = item.field_clear or 'clear_upload' %} + + {{ form.image_upload(data, errors, is_upload_enabled=item.upload_enabled, is_url=is_url, is_upload=is_upload, upload_label = _('Site logo'), url_label=_('Site logo'), + field_url=field_url, field_upload=field_upload, field_clear=field_clear)}} + {% else %} + {% call form[control](name, id=id, label=label, placeholder=placeholder, value=value, error=error, classes=classes) %} + {% if item.extra_info %}{{ form.info(item.extra_info) }}{% endif %} + {% endcall %} + {% endif %} + {% endfor %} +{%- endmacro -%} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form.html new file mode 100644 index 0000000..f33b7ab --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form.html @@ -0,0 +1,36 @@ +{# +All macros were split into their own template file in `templates/macros/form/`) +and here, we are importing them all to maintain backward compatibility. +#} + +{% from 'macros/form/input.html' import input %} +{% from "macros/form/input_block.html" import input_block %} +{% from 'macros/form/checkbox.html' import checkbox %} +{% from 'macros/form/select.html' import select %} +{% from "macros/form/attributes.html" import attributes %} +{% from "macros/form/markdown.html" import markdown %} +{% from "macros/form/textarea.html" import textarea %}s +{% from "macros/form/prepend.html" import prepend %} +{% from "macros/form/custom.html" import custom %} +{% from "macros/form/errors.html" import errors %} +{% from "macros/form/info.html" import info %} +{% from "macros/form/hidden.html" import hidden %} +{% from "macros/form/hidden_from_list.html" import hidden_from_list %} +{% from "macros/form/required_message.html" import required_message %} +{% from "macros/form/image_upload.html" import image_upload %} + +{% set input = input %} +{% set input_block = input_block %} +{% set checkbox = checkbox %} +{% set select = select %} +{% set attributes = attributes %} +{% set markdown = markdown %} +{% set textarea = textarea %} +{% set prepend = prepend %} +{% set custom = custom %} +{% set errors = errors %} +{% set info = info %} +{% set hidden = hidden %} +{% set hidden_from_list = hidden_from_list %} +{% set required_message = required_message %} +{% set image_upload = image_upload %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/attributes.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/attributes.html new file mode 100644 index 0000000..47359a6 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/attributes.html @@ -0,0 +1,17 @@ +{# +Builds a space seperated list of html attributes from a dict of key/value pairs. +Generally only used internally by macros. + +attrs - A dict of attribute/value pairs + +Example + +{% import 'macros/form.html' as form %} +{{ form.attributes({}) }} + +#} +{%- macro attributes(attrs={}) -%} +{%- for key, value in attrs.items() -%} +{{ " " }}{{ key }}{% if value != "" %}="{{ value }}"{% endif %} +{%- endfor -%} +{%- endmacro -%} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/checkbox.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/checkbox.html new file mode 100644 index 0000000..130720f --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/checkbox.html @@ -0,0 +1,34 @@ +{% from "macros/form/attributes.html" import attributes %} + +{# +Builds a single checkbox input. + +name - The name of the form parameter. +id - The id to use on the input and label. Convention is to prefix with 'field-'. +label - The human readable label. +value - The value of the input. +checked - If true the checkbox will be checked +error - An error string for the field or just true to highlight the field. +classes - An array of classes to apply to the form-group. +is_required - Boolean of whether this input is requred for the form to validate + +Example: + +{% import 'macros/form.html' as form %} +{{ form.checkbox('remember', checked=true) }} + +#} +{% macro checkbox(name, id='', label='', value='', checked=false, placeholder='', error="", classes=[], attrs={}, is_required=false) %} +{%- set extra_html = caller() if caller -%} +
    +
    + + {{ extra_html }} +
    +
    +{% endmacro %} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/custom.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/custom.html new file mode 100644 index 0000000..4503b05 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/custom.html @@ -0,0 +1,61 @@ +{% from "macros/form/input_block.html" import input_block %} +{% from "macros/form/attributes.html" import attributes %} + +{# +Creates all the markup required for an custom key/value input. These are usually +used to let the user provide custom meta data. Each "field" has three inputs +one for the key, one for the value and a checkbox to remove it. So the arguments +for this macro are nearly all tuples containing values for the +(key, value, delete) fields respectively. + +name - A tuple of names for the three fields. +id - An id string to be used for each input. +label - The human readable label for the main label. +values - A tuple of values for the (key, value, delete) fields. If delete +is truthy the checkbox will be checked. +placeholder - A tuple of placeholder text for the (key, value) fields. +error - A list of error strings for the field or just true to highlight the field. +classes - An array of classes to apply to the form-group. +is_required - Boolean of whether this input is requred for the form to validate + +Examples: + +{% import 'macros/form.html' as form %} +{{ form.custom( +names=('custom_key', 'custom_value', 'custom_deleted'), +id='field-custom', +label=_('Custom Field'), +values=(extra.key, extra.value, extra.deleted), +error='' +) }} +#} +{% macro custom(names=(), id="", label="", values=(), placeholders=(), error="", classes=[], attrs={}, is_required=false, key_values=()) %} +{%- set classes = (classes|list) -%} +{%- set label_id = (id or names[0]) ~ "-key" -%} +{%- set extra_html = caller() if caller -%} +{%- do classes.append('control-custom') -%} + +{% call input_block(label_id, label or name, error, classes, control_classes=["editor"], extra_html=extra_html, is_required=is_required) %} +
    +
    +
    + + +
    +
    +
    + {% if values[0] or values[1] or error %} + + {% endif %} +
    + + +
    +
    +
    + +{% endcall %} +{% endmacro %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/errors.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/errors.html new file mode 100644 index 0000000..9f55bad --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/errors.html @@ -0,0 +1,26 @@ +{# +Builds a list of errors for the current form. + +errors - A dict of field/message pairs. +type - The alert-* class that should be applied (default: "error") +classes - A list of classes to apply to the wrapper (default: []) + +Example: + +{% import 'macros/form.html' as form %} +{{ form.errors(error_summary, type="warning") }} + +#} + +{% macro errors(errors={}, type="error", classes=[]) %} +{% if errors %} +
    +

    {{ _('The form contains invalid entries:') }}

    +
      + {% for key, error in errors.items() %} +
    • {% if key %}{{ key }}: {% endif %}{{ error }}
    • + {% endfor %} +
    +
    +{% endif %} +{% endmacro %} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/hidden.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/hidden.html new file mode 100644 index 0000000..ad46447 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/hidden.html @@ -0,0 +1,14 @@ +{# +Builds a single hidden input. + +name - name of the hidden input +value - value of the hidden input + +Example +{% import 'macros/form.html' as form %} +{{ form.hidden('name', 'value') }} + +#} +{% macro hidden(name, value) %} + +{% endmacro %} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/hidden_from_list.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/hidden_from_list.html new file mode 100644 index 0000000..60e0d16 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/hidden_from_list.html @@ -0,0 +1,28 @@ +{% from "macros/form/hidden.html" import hidden %} + +{# +Contructs hidden inputs for each name-value pair. + +fields - [('name1', 'value1'), ('name2', 'value2'), ...] + +Two parameter for excluding several names or name-value pairs. + +except_names - list of names to be excluded +except - list of name-value pairs to be excluded + + +Example: +{% import 'macros/form.html' as form %} +{% form.hidden_from_list(fields=c.fields, except=[('topic', 'xyz')]) %} +{% form.hidden_from_list(fields=c.fields, except_names=['time_min', 'time_max']) %} +#} +{% macro hidden_from_list(fields, except_names=None, except=None) %} +{% set except_names = except_names or [] %} +{% set except = except or [] %} + +{% for name, value in fields %} +{% if name and value and name not in except_names and (name, value) not in except %} +{{ hidden(name, value) }} +{% endif %} +{% endfor %} +{% endmacro %} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/image_upload.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/image_upload.html new file mode 100644 index 0000000..1f35a0b --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/image_upload.html @@ -0,0 +1,59 @@ +{% from 'macros/form/input.html' import input %} +{% from 'macros/form/checkbox.html' import checkbox %} + +{# +Builds a file upload for input + +Example +{% import 'macros/form.html' as form %} +{{ form.image_upload(data, errors, is_upload_enabled=true) }} + +#} +{% macro image_upload(data, errors, field_url='image_url', field_upload='image_upload', field_clear='clear_upload', + is_url=false, is_upload=false, is_upload_enabled=false, placeholder=false, + url_label='', upload_label='', field_name='image_url') %} +{% set placeholder = placeholder if placeholder else _('http://example.com/my-image.jpg') %} +{% set url_label = url_label or _('Image URL') %} +{% set upload_label = upload_label or _('Image') %} +{% set previous_upload = data['previous_upload'] %} + +{% if field_url == 'url' and field_upload == 'upload' %} + {# backwards compatibility for old resource forms that still call the `forms.image_upload()` macro, eg ckanext-scheming #} + {% snippet 'package/snippets/resource_upload_field.html', + data=data, + errors=errors, + is_url=is_url, + is_upload=is_upload, + is_upload_enabled=is_upload_enabled, + url_label=url_label, + upload_label=upload_label, + placeholder=placeholder %} +{% else %} + {% if is_upload_enabled %} +
    + {% endif %} + + + {{ input(field_url, label=url_label, id='field-image-url', type='url', placeholder=placeholder, value=data.get(field_url), error=errors.get(field_url), classes=['control-full']) }} + + + {% if is_upload_enabled %} + {{ input(field_upload, label=upload_label, id='field-image-upload', type='file', placeholder='', value='', error='', classes=['control-full']) }} + {% if is_upload %} + {{ checkbox(field_clear, label=_('Clear Upload'), id='field-clear-upload', value='true', error='', classes=['control-full']) }} + {% endif %} + {% endif %} + + {% if is_upload_enabled %}
    {% endif %} +{% endif %} + +{% endmacro %} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/info.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/info.html new file mode 100644 index 0000000..54ecc84 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/info.html @@ -0,0 +1,24 @@ +{# +Renders an info box with a description. This will usually be used with in a +call block when creating an input element. + +text - The text to include in the box. +inline - If true displays the info box inline with the input. +classes - A list of classes to add to the info box. + +Example + +{% import 'macros/form.html' as form %} +{% call form.input('name') %} +{{ form.info(_('My useful help text')) }} +{% endcall %} + +#} +{% macro info(text='', inline=false, classes=[]) %} +{%- if text -%} +
    + +{{ text }} +
    +{%- endif -%} +{% endmacro %} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/input.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/input.html new file mode 100644 index 0000000..d7f7c29 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/input.html @@ -0,0 +1,30 @@ +{% from "macros/form/input_block.html" import input_block %} +{% from "macros/form/attributes.html" import attributes %} + +{# +Creates all the markup required for an input element. Handles matching labels to +inputs, error messages and other useful elements. + +name - The name of the form parameter. +id - The id to use on the input and label. Convention is to prefix with 'field-'. +label - The human readable label. +value - The value of the input. +placeholder - Some placeholder text. +type - The type of input eg. email, url, date (default: text). +error - A list of error strings for the field or just true to highlight the field. +classes - An array of classes to apply to the form-group. +is_required - Boolean of whether this input is requred for the form to validate + +Examples: + +{% import 'macros/form.html' as form %} +{{ form.input('title', label=_('Title'), value=data.title, error=errors.title) }} + +#} +{% macro input(name, id='', label='', value='', placeholder='', type='text', error="", classes=[], attrs={'class': 'form-control'}, is_required=false) %} +{%- set extra_html = caller() if caller -%} + +{% call input_block(id or name, label or name, error, classes, extra_html=extra_html, is_required=is_required) %} + +{% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/input_block.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/input_block.html new file mode 100644 index 0000000..6f7e691 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/input_block.html @@ -0,0 +1,32 @@ +{# +A generic input_block for providing the default markup for CKAN form elements. +It is expected to be called using a {% call %} block, the contents of which +will be inserted into the .controls element. + +for - The id for the input that the label should match. +label - A human readable label. +error - A list of error strings for the field or just true. +classes - An array of custom classes for the outer element. +control_classes - An array of custom classes for the .control wrapper. +extra_html - An html string to be inserted after the errors eg. info text. +is_required - Boolean of whether this input is requred for the form to validate + +Example: + +{% import 'macros/form.html' as form %} +{% call form.input_block("field", "My Field") %} + +{% endcall %} + +#} + +{% macro input_block(for, label="", error="", classes=[], control_classes=[], extra_html="", is_required=false) %} +
    + +
    +{{ caller() }} +{% if error and error is iterable %}{{ error|join(', ') }}{% endif %} +{{ extra_html }} +
    +
    +{% endmacro %} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/markdown.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/markdown.html new file mode 100644 index 0000000..59b4a1d --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/markdown.html @@ -0,0 +1,33 @@ +{% from "macros/form/input_block.html" import input_block %} +{% from "macros/form/attributes.html" import attributes %} + +{# +Creates all the markup required for a Markdown textarea element. Handles +matching labels to inputs, selected item and error messages. + +name - The name of the form parameter. +id - The id to use on the input and label. Convention is to prefix with 'field-'. +label - The human readable label. +value - The value of the input. +placeholder - Some placeholder text. +error - A list of error strings for the field or just true to highlight the field. +classes - An array of classes to apply to the form-group. +is_required - Boolean of whether this input is requred for the form to validate + +Examples: + +{% import 'macros/form.html' as form %} +{{ form.markdown('desc', id='field-description', label=_('Description'), value=data.desc, error=errors.desc) }} + +#} +{% macro markdown(name, id='', label='', value='', placeholder='', error="", classes=[], attrs={'class': 'form-control'}, is_required=false) %} +{% set classes = (classes|list) %} +{% do classes.append('control-full') %} +{% set markdown_tooltip = "

    __Bold text__ or _italic text_

    # title
    ## secondary title
    ### etc

    * list
    * of
    * items

    http://auto.link.ed/

    Full markdown syntax

    Please note: HTML tags are stripped out for security reasons

    " %} + +{%- set extra_html = caller() if caller -%} +{% call input_block(id or name, label or name, error, classes, control_classes=["editor"], extra_html=extra_html, is_required=is_required) %} + +{% trans %}You can use Markdown formatting here{% endtrans %} +{% endcall %} +{% endmacro %} diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/prepend.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/prepend.html new file mode 100644 index 0000000..1441343 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/prepend.html @@ -0,0 +1,38 @@ +{% from "macros/form/input_block.html" import input_block %} +{% from "macros/form/attributes.html" import attributes %} + +{# +Creates all the markup required for an input element with a prefixed segment. +These are useful for showing url slugs and other fields where the input +information forms only part of the saved data. + +name - The name of the form parameter. +id - The id to use on the input and label. Convention is to prefix with 'field-'. +label - The human readable label. +prepend - The text that will be prepended before the input. +value - The value of the input. +which will use the name key as the value. +placeholder - Some placeholder text. +error - A list of error strings for the field or just true to highlight the field. +classes - An array of classes to apply to the form-group. +is_required - Boolean of whether this input is requred for the form to validate + +Examples: + +{% import 'macros/form.html' as form %} +{{ form.prepend('slug', id='field-slug', prepend='/dataset/', label=_('Slug'), value=data.slug, error=errors.slug) }} + +#} +{% macro prepend(name, id='', label='', prepend='', value='', placeholder='', type='text', error="", classes=[], attrs={'class': 'form-control'}, is_required=false) %} +{# We manually append the error here as it needs to be inside the .input-group block #} +{% set classes = (classes|list) %} +{% do classes.append('error') if error %} +{%- set extra_html = caller() if caller -%} +{% call input_block(id or name, label or name, error='', classes=classes, extra_html=extra_html, is_required=is_required) %} +
    + {% if prepend %}{%- endif -%} + + {% if error and error is iterable %}{{ error|join(', ') }}{% endif %} +
    +{% endcall %} +{% endmacro %} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/required_message.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/required_message.html new file mode 100644 index 0000000..f5a4cc7 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/required_message.html @@ -0,0 +1,13 @@ +{# +Outputs the "* Required field" message for the bottom of formss + +Example +{% import 'macros/form.html' as form %} +{{ form.required_message() }} + +#} +{% macro required_message() %} +

    + * {{ _("Required field") }} +

    +{% endmacro %} \ No newline at end of file diff --git a/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/select.html b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/select.html new file mode 100644 index 0000000..15a1025 --- /dev/null +++ b/compose/2.10/src/ckanext-d4science_theme/ckanext/d4science_theme/templates/macros/form/select.html @@ -0,0 +1,50 @@ +{% from "macros/form/input_block.html" import input_block %} +{% from "macros/form/attributes.html" import attributes %} + +{# +Creates all the markup required for an select element. Handles matching labels to +inputs and error messages. + +A field should be a dict with a "value" key and an optional "text" key which +will be displayed to the user. We use a dict to easily allow extension in +future should extra options be required. + +name - The name of the form parameter. +id - The id to use on the input and label. Convention is to prefix with 'field-'. +label - The human readable label. +options - A list/tuple of fields to be used as . + selected - The value of the selected

    You are viewing an HTML version of the XML OAI response. More information about this XSLT.
    Return to OAI-PMH 2.0 Home for this Repository