diff --git a/.ahoy.yml b/.ahoy.yml new file mode 100644 index 00000000..02a53cb8 --- /dev/null +++ b/.ahoy.yml @@ -0,0 +1,190 @@ +--- +ahoyapi: v2 + +commands: + + # Docker commands. + build: + usage: Build or rebuild project. + cmd: | + ahoy title "Building project" + ahoy pre-flight + ahoy clean + ahoy build-network + ahoy up -- --build --force-recreate + ahoy install-site + ahoy title "Build complete" + ahoy doctor + ahoy info 1 + + build-network: + usage: Ensure that the amazeeio network exists. + cmd: | + docker network prune -f > /dev/null + docker network inspect amazeeio-network > /dev/null || docker network create amazeeio-network + + info: + usage: Print information about this project. + cmd: | + ahoy line "Project : " ${PROJECT} + ahoy line "Site local URL : " ${LAGOON_LOCALDEV_URL} + ahoy line "DB port on host : " $(docker port $(docker-compose ps -q postgres) 5432 | cut -d : -f 2) + ahoy line "Solr port on host : " $(docker port $(docker-compose ps -q solr) 8983 | cut -d : -f 2) + ahoy line "Mailhog URL : " http://mailhog.docker.amazee.io/ + + up: + usage: Build and start Docker containers. + cmd: | + docker-compose up -d "$@" + sleep 10 + docker-compose logs + ahoy cli "dockerize -wait tcp://ckan:3000 -timeout 1m" + if docker-compose logs | grep -q "\[Error\]"; then docker-compose logs; exit 1; fi + if docker-compose logs | grep -q "Exception"; then docker-compose logs; exit 1; fi + docker ps -a --filter name=^/${COMPOSE_PROJECT_NAME}_ + export DOCTOR_CHECK_CLI=0 + + down: + usage: Stop Docker containers and remove container, images, volumes and networks. + cmd: 'if [ -f "docker-compose.yml" ]; then docker-compose down --volumes; fi' + + start: + usage: Start existing Docker containers. + cmd: docker-compose start "$@" + + stop: + usage: Stop running Docker containers. + cmd: docker-compose stop "$@" + + restart: + usage: Restart all stopped and running Docker containers. + cmd: docker-compose restart "$@" + + logs: + usage: Show Docker logs. + cmd: docker-compose logs "$@" + + pull: + usage: Pull latest docker images. + cmd: if [ ! -z "$(docker image ls -q)" ]; then docker image ls --format \"{{.Repository}}:{{.Tag}}\" | grep amazeeio/ | grep -v none | xargs -n1 docker pull | cat; fi + + cli: + usage: Start a shell inside CLI container or run a command. + cmd: if \[ "${#}" -ne 0 \]; then docker exec $(docker-compose ps -q ckan) sh -c '. ${VENV_DIR}/bin/activate; cd $APP_DIR;'" $*"; else docker exec $(docker-compose ps -q ckan) sh -c '. ${VENV_DIR}/bin/activate && cd $APP_DIR && sh'; fi + + doctor: + usage: Find problems with current project setup. + cmd: .docker/scripts/doctor.sh "$@" + + install-site: + usage: Install a site. + cmd: | + ahoy title "Installing a fresh site" + ahoy cli "./scripts/init.sh" + + clean: + usage: Remove containers and all build files. + cmd: | + ahoy down + # Remove other directories. + # @todo: Add destinations below. + rm -rf \ + ./ckan + + reset: + usage: "Reset environment: remove containers, all build, manually created and Drupal-Dev files." + cmd: | + ahoy clean + git ls-files --others -i --exclude-from=.git/info/exclude | xargs chmod 777 + git ls-files --others -i --exclude-from=.git/info/exclude | xargs rm -Rf + find . -type d -not -path "./.git/*" -empty -delete + + flush-redis: + usage: Flush Redis cache. + cmd: docker exec -i $(docker-compose ps -q redis) redis-cli flushall > /dev/null + + lint: + usage: Lint code. + cmd: | + ahoy cli "flake8 ${@:-ckanext}" || \ + [ "${ALLOW_LINT_FAIL:-0}" -eq 1 ] + + test-unit: + usage: Run unit tests. + cmd: | + ahoy cli 'pytest --ckan-ini=${CKAN_INI}' || \ + [ "${ALLOW_UNIT_FAIL:-0}" -eq 1 ] + + test-bdd: + usage: Run BDD tests. + cmd: | + ahoy start-ckan-job-worker & + ahoy start-mailmock & + sleep 5 && + ahoy cli "behave -k ${*:-test/features}" --tags @smoke && \ + ahoy cli "behave -k ${*:-test/features}" || \ + [ "${ALLOW_BDD_FAIL:-0}" -eq 1 ] + ahoy stop-mailmock + ahoy stop-ckan-job-worker + + start-mailmock: + usage: Starts email mock server used for email BDD tests + cmd: | + ahoy title 'Starting mailmock' + ahoy cli 'mailmock -p 8025 -o ${APP_DIR}/test/emails --no-stdout' # for debugging mailmock email output remove --no-stdout + + stop-mailmock: + usage: Stops email mock server used for email BDD tests + cmd: | + ahoy title 'Stopping mailmock' + ahoy cli "killall -2 mailmock" + + start-ckan-job-worker: + usage: Starts default CKAN background job worker + cmd: | + ahoy title 'Starting default CKAN background job worker' + ahoy cli "ckan_cli jobs clear && \ + ckan_cli jobs worker" + + stop-ckan-job-worker: + usage: Stops CKAN background job worker + cmd: | + ahoy title 'Stopping CKAN background job worker' + ahoy cli "pkill -f 'jobs worker'" + + # Utilities. + title: + cmd: printf "$(tput -Txterm setaf 4)==> ${1}$(tput -Txterm sgr0)\n" + hide: true + + line: + cmd: printf "$(tput -Txterm setaf 2)${1}$(tput -Txterm sgr0)${2}\n" + hide: true + + getvar: + cmd: eval echo "${@}" + hide: true + + pre-flight: + cmd: | + export DOCTOR_CHECK_DB=${DOCTOR_CHECK_DB:-1} + export DOCTOR_CHECK_TOOLS=${DOCTOR_CHECK_TOOLS:-1} + export DOCTOR_CHECK_PORT=${DOCTOR_CHECK_PORT:-0} + export DOCTOR_CHECK_PYGMY=${DOCTOR_CHECK_PYGMY:-1} + export DOCTOR_CHECK_CLI=${DOCTOR_CHECK_CLI:-0} + export DOCTOR_CHECK_SSH=${DOCTOR_CHECK_SSH:-0} + export DOCTOR_CHECK_WEBSERVER=${DOCTOR_CHECK_WEBSERVER:-0} + export DOCTOR_CHECK_BOOTSTRAP=${DOCTOR_CHECK_BOOTSTRAP:-0} + ahoy doctor + hide: true + +entrypoint: + - bash + - "-c" + - "-e" + - | + export LAGOON_LOCALDEV_URL=http://$PROJECT.docker.amazee.io + [ -f .env ] && [ -s .env ] && export $(grep -v '^#' .env | xargs) && if [ -f .env.local ] && [ -s .env.local ]; then export $(grep -v '^#' .env.local | xargs); fi + bash -e -c "$0" "$@" + - "{{cmd}}" + - "{{name}}" diff --git a/.circleci/build.sh b/.circleci/build.sh new file mode 100755 index 00000000..407dd0ec --- /dev/null +++ b/.circleci/build.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +## +# Build site in CI. +# +set -e + +# Process Docker Compose configuration. This is used to avoid multiple +# docker-compose.yml files. +# Remove lines containing '###'. +sed -i -e "/###/d" docker-compose.yml +# Uncomment lines containing '##'. +sed -i -e "s/##//" docker-compose.yml + +# Pull the latest images. +ahoy pull + +# Disable checks used on host machine. +export DOCTOR_CHECK_PYGMY=0 +export DOCTOR_CHECK_PORT=0 +export DOCTOR_CHECK_SSH=0 +export DOCTOR_CHECK_WEBSERVER=0 +export DOCTOR_CHECK_BOOTSTRAP=0 + +ahoy build || (ahoy logs; exit 1) diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..c3be0c63 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,43 @@ +version: 2 +aliases: + + # Shared configuration applied to each job. + - &container_config + working_directory: /app + docker: + #; Using "runner" container where each job will be executed. This container + #; has all necessary tools to run dockerized environment. + #; @see https://github.com/integratedexperts/ci-builder + - image: integratedexperts/ci-builder + + # Step to setup remote docker. + - &step_setup_remote_docker + setup_remote_docker + +jobs: + build: + <<: *container_config + parallelism: 1 + steps: + - attach_workspace: + at: /workspace + - checkout + - *step_setup_remote_docker + - run: + command: .circleci/build.sh + environment: + CKAN_VERSION: 2.9.5 + - run: .circleci/test.sh + - run: + name: Process artifacts + command: .circleci/process-artifacts.sh + when: always + - store_artifacts: + path: /tmp/artifacts + when: always + +workflows: + version: 2 + main: + jobs: + - build diff --git a/.circleci/process-artifacts.sh b/.circleci/process-artifacts.sh new file mode 100755 index 00000000..55bdbef9 --- /dev/null +++ b/.circleci/process-artifacts.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +## +# Process test artifacts. +# +set -e + +# Create screenshots directory in case it was not created before. This is to +# avoid this script to fail when copying artifacts. +ahoy cli "mkdir -p test/screenshots" + +# Copy from the app container to the build host for storage. +mkdir -p /tmp/artifacts/behave +docker cp "$(docker-compose ps -q ckan)":/app/test/screenshots /tmp/artifacts/behave/ diff --git a/.circleci/test.sh b/.circleci/test.sh new file mode 100755 index 00000000..9c584d9b --- /dev/null +++ b/.circleci/test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +## +# Run tests in CI. +# +set -e + +echo "==> Lint code" +ahoy lint + +echo "==> Run Unit tests" +ahoy test-unit + +echo "==> Run BDD tests" +ahoy test-bdd || (ahoy logs; exit 1) diff --git a/.docker/Dockerfile.ckan b/.docker/Dockerfile.ckan new file mode 100644 index 00000000..5fc19a38 --- /dev/null +++ b/.docker/Dockerfile.ckan @@ -0,0 +1,47 @@ +FROM amazeeio/python:2.7-ckan-21.8.0 + +ARG SITE_URL=http://ckan:3000/ +ARG CKAN_REPO=ckan/ckan +ARG CKAN_VERSION=2.8.8 +ENV SITE_URL="${SITE_URL}" +ENV VENV_DIR=/app/ckan/default +ENV APP_DIR=/app +ENV CKAN_INI=/app/ckan/default/production.ini + +WORKDIR "${APP_DIR}" + +ENV DOCKERIZE_VERSION v0.6.1 +RUN apk add --no-cache curl build-base \ + && curl -sLO https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz \ + && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz \ + && rm dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz + +# Install CKAN. + +RUN . ${VENV_DIR}/bin/activate \ + && pip install setuptools==36.1 \ + && pip install -e "git+https://github.com/${CKAN_REPO}.git@ckan-${CKAN_VERSION}#egg=ckan" \ + && sed -i "s/psycopg2==2.4.5/psycopg2==2.7.7/g" "${VENV_DIR}/src/ckan/requirements.txt" \ + && ((test -f "${VENV_DIR}/src/ckan/requirements-py2.txt" && \ + pip install -r "${VENV_DIR}/src/ckan/requirements-py2.txt") || \ + pip install -r "${VENV_DIR}/src/ckan/requirements.txt") \ + && ln -s "${VENV_DIR}/src/ckan/who.ini" "${VENV_DIR}/who.ini" \ + && deactivate \ + && ln -s ${APP_DIR}/ckan /usr/lib/ckan \ + && fix-permissions ${APP_DIR}/ckan + +COPY .docker/test.ini $CKAN_INI + +COPY . ${APP_DIR}/ + +COPY .docker/scripts ${APP_DIR}/scripts + +COPY .docker/scripts/ckan_cli ${VENV_DIR}/bin/ + +RUN chmod +x ${APP_DIR}/scripts/*.sh ${VENV_DIR}/bin/ckan_cli + +# Init current extension. +RUN ${APP_DIR}/scripts/init-ext.sh + +ENTRYPOINT ["/sbin/tini", "--", "/lagoon/entrypoints.sh"] +CMD ["/app/scripts/serve.sh"] diff --git a/.docker/scripts/ckan_cli b/.docker/scripts/ckan_cli new file mode 100644 index 00000000..3cf0b4cc --- /dev/null +++ b/.docker/scripts/ckan_cli @@ -0,0 +1,75 @@ +#!/bin/sh + +# Call either 'ckan' (from CKAN >= 2.9) or 'paster' (from CKAN <= 2.8) +# with appropriate syntax, depending on what is present on the system. +# This is intended to smooth the upgrade process from 2.8 to 2.9. +# Eg: +# ckan_cli jobs list +# could become either: +# paster --plugin=ckan jobs list -c /etc/ckan/default/production.ini +# or: +# ckan -c /etc/ckan/default/production.ini jobs list + +# This script is aware of the VIRTUAL_ENV environment variable, and will +# attempt to respect it with similar behaviour to commands like 'pip'. +# Eg placing this script in a virtualenv 'bin' directory will cause it +# to call the 'ckan' or 'paster' command in that directory, while +# placing this script elsewhere will cause it to rely on the VIRTUAL_ENV +# variable, or if that is not set, the system PATH. + +# Since the positioning of the CKAN configuration file is central to the +# differences between 'paster' and 'ckan', this script needs to be aware +# of the config file location. It will use the CKAN_INI environment +# variable if it exists, or default to /etc/ckan/default/production.ini. + +# If 'paster' is being used, the default plugin is 'ckan'. A different +# plugin can be specified by setting the PASTER_PLUGIN environment +# variable. This variable is irrelevant if using the 'ckan' command. + +CKAN_INI="${CKAN_INI:-/etc/ckan/default/production.ini}" +PASTER_PLUGIN="${PASTER_PLUGIN:-ckan}" +# First, look for a command alongside this file +ENV_DIR=$(dirname "$0") +if [ -f "$ENV_DIR/ckan" ]; then + COMMAND=ckan +elif [ -f "$ENV_DIR/paster" ]; then + COMMAND=paster +elif [ "$VIRTUAL_ENV" != "" ]; then + # If command not found alongside this file, check the virtualenv + ENV_DIR="$VIRTUAL_ENV/bin" + if [ -f "$ENV_DIR/ckan" ]; then + COMMAND=ckan + elif [ -f "$ENV_DIR/paster" ]; then + COMMAND=paster + fi +else + # if no virtualenv is active, try the system path + if (which ckan > /dev/null 2>&1); then + ENV_DIR=$(dirname $(which ckan)) + COMMAND=ckan + elif (which paster > /dev/null 2>&1); then + ENV_DIR=$(dirname $(which paster)) + COMMAND=paster + else + echo "Unable to locate 'ckan' or 'paster' command" >&2 + exit 1 + fi +fi + +if [ "$COMMAND" = "ckan" ]; then + echo "Using 'ckan' command from $ENV_DIR with config ${CKAN_INI}..." >&2 + # adjust args to match ckan expectations + COMMAND=$(echo "$1" | sed -e 's/create-test-data/seed/') + shift + exec $ENV_DIR/ckan -c ${CKAN_INI} $COMMAND "$@" $CLICK_ARGS +elif [ "$COMMAND" = "paster" ]; then + echo "Using 'paster' command from $ENV_DIR with config ${CKAN_INI}..." >&2 + # adjust args to match paster expectations + COMMAND=$1 + shift + if [ "$1" = "show" ]; then shift; fi + exec $ENV_DIR/paster --plugin=$PASTER_PLUGIN $COMMAND "$@" -c ${CKAN_INI} +else + echo "Unable to locate 'ckan' or 'paster' command in $ENV_DIR" >&2 + exit 1 +fi diff --git a/.docker/scripts/create-test-data.sh b/.docker/scripts/create-test-data.sh new file mode 100644 index 00000000..c4a08f6f --- /dev/null +++ b/.docker/scripts/create-test-data.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env sh +## +# Create some example content for extension BDD tests. +# +set -e + +CKAN_ACTION_URL=http://ckan:3000/api/action + +if [ "$VENV_DIR" != "" ]; then + . ${VENV_DIR}/bin/activate +fi + +CKAN_USER_NAME="${CKAN_USER_NAME:-admin}" +CKAN_DISPLAY_NAME="${CKAN_DISPLAY_NAME:-Administrator}" +CKAN_USER_EMAIL="${CKAN_USER_EMAIL:-admin@localhost}" + +add_user_if_needed () { + echo "Adding user '$2' ($1) with email address [$3]" + ckan_cli user show "$1" | grep "$1" || ckan_cli user add "$1"\ + fullname="$2"\ + email="$3"\ + password="${4:-Password123!}" +} + +add_user_if_needed "$CKAN_USER_NAME" "$CKAN_DISPLAY_NAME" "$CKAN_USER_EMAIL" +ckan_cli sysadmin add "${CKAN_USER_NAME}" + +# We know the "admin" sysadmin account exists, so we'll use her API KEY to create further data +API_KEY=$(ckan_cli user show "${CKAN_USER_NAME}" | tr -d '\n' | sed -r 's/^(.*)apikey=(\S*)(.*)/\2/') +if [ "$API_KEY" = "None" ]; then + echo "No API Key found on ${CKAN_USER_NAME}, generating API Token..." + API_KEY=$(ckan_cli user token add "${CKAN_USER_NAME}" test_setup |grep -v '^API Token created' | tr -d '[:space:]') +fi + +## +# BEGIN: Add sysadmin config values. +# This needs to be done before closing datarequests as they require the below config values +# +echo "Adding ckan.datarequests.closing_circumstances:" + +curl -LsH "Authorization: ${API_KEY}" \ + --header "Content-Type: application/json" \ + --data '{"ckan.datarequests.closing_circumstances":"Released as open data|nominate_dataset\nOpen dataset already exists|nominate_dataset\nPartially released|nominate_dataset\nTo be released as open data at a later date|nominate_approximate_date\nData openly available elsewhere\nNot suitable for release as open data\nRequested data not available/cannot be compiled\nRequestor initiated closure"}' \ + ${CKAN_ACTION_URL}/config_option_update + +## +# END. +# + +## +# BEGIN: Create a test organisation with test users for admin, editor and member +# +TEST_ORG_NAME=test-organisation +TEST_ORG_TITLE="Test Organisation" + +echo "Creating test users for ${TEST_ORG_TITLE} Organisation:" + +add_user_if_needed ckan_user "CKAN User" ckan_user@localhost +add_user_if_needed test_org_admin "Test Admin" test_org_admin@localhost +add_user_if_needed test_org_editor "Test Editor" test_org_editor@localhost +add_user_if_needed test_org_member "Test Member" test_org_member@localhost + +echo "Creating ${TEST_ORG_TITLE} Organisation:" + +TEST_ORG=$( \ + curl -LsH "Authorization: ${API_KEY}" \ + --data "name=${TEST_ORG_NAME}&title=${TEST_ORG_TITLE}" \ + ${CKAN_ACTION_URL}/organization_create +) + +TEST_ORG_ID=$(echo $TEST_ORG | sed -r 's/^(.*)"id": "(.*)",(.*)/\2/') + +echo "Assigning test users to ${TEST_ORG_TITLE} Organisation:" + +curl -LsH "Authorization: ${API_KEY}" \ + --data "id=${TEST_ORG_ID}&object=test_org_admin&object_type=user&capacity=admin" \ + ${CKAN_ACTION_URL}/member_create + +curl -LsH "Authorization: ${API_KEY}" \ + --data "id=${TEST_ORG_ID}&object=test_org_editor&object_type=user&capacity=editor" \ + ${CKAN_ACTION_URL}/member_create + +curl -LsH "Authorization: ${API_KEY}" \ + --data "id=${TEST_ORG_ID}&object=test_org_member&object_type=user&capacity=member" \ + ${CKAN_ACTION_URL}/member_create +## +# END. +# + +## +# BEGIN: Create a Data Request organisation with test users for admin, editor and member and default data requests +# +# Data Requests requires a specific organisation to exist in order to create DRs for Data.Qld +DR_ORG_NAME=open-data-administration-data-requests +DR_ORG_TITLE="Open Data Administration (data requests)" + +echo "Creating test users for ${DR_ORG_TITLE} Organisation:" + +add_user_if_needed dr_admin "Data Request Admin" dr_admin@localhost +add_user_if_needed dr_editor "Data Request Editor" dr_editor@localhost +add_user_if_needed dr_member "Data Request Member" dr_member@localhost + +echo "Creating ${DR_ORG_TITLE} Organisation:" + +DR_ORG=$( \ + curl -LsH "Authorization: ${API_KEY}" \ + --data "name=${DR_ORG_NAME}&title=${DR_ORG_TITLE}" \ + ${CKAN_ACTION_URL}/organization_create +) + +DR_ORG_ID=$(echo $DR_ORG | sed -r 's/^(.*)"id": "(.*)",(.*)/\2/') + +echo "Assigning test users to ${DR_ORG_TITLE} Organisation:" + +curl -LsH "Authorization: ${API_KEY}" \ + --data "id=${DR_ORG_ID}&object=dr_admin&object_type=user&capacity=admin" \ + ${CKAN_ACTION_URL}/member_create + +curl -LsH "Authorization: ${API_KEY}" \ + --data "id=${DR_ORG_ID}&object=dr_editor&object_type=user&capacity=editor" \ + ${CKAN_ACTION_URL}/member_create + +curl -LsH "Authorization: ${API_KEY}" \ + --data "id=${DR_ORG_ID}&object=dr_member&object_type=user&capacity=member" \ + ${CKAN_ACTION_URL}/member_create + + +echo "Creating test Data Request:" + +curl -LsH "Authorization: ${API_KEY}" \ + --data "title=Test Request&description=This is an example&organization_id=${TEST_ORG_ID}" \ + ${CKAN_ACTION_URL}/create_datarequest + +echo "Creating closed Data Request:" + +Closed_DR=$( \ + curl -LsH "Authorization: ${API_KEY}" \ + --data "title=Closed Request&description=This is an example&organization_id=${DR_ORG_ID}" \ + ${CKAN_ACTION_URL}/create_datarequest \ +) + +echo $Closed_DR + +# # Get the ID of that newly created Data Request +CLOSE_DR_ID=$(echo $Closed_DR | tr -d '\n' | sed -r 's/^(.*)}, "id": "([a-z0-9\-]*)",(.*)/\2/') +echo $CLOSE_DR_ID + +echo "Closing Data Request:" + +curl -LsH "Authorization: ${API_KEY}" \ + --data "id=${CLOSE_DR_ID}&close_circumstance=Requestor initiated closure" \ + ${CKAN_ACTION_URL}/close_datarequest + +## +# END. +# + +# Use CKAN's built-in commands for creating some test datasets... +ckan_cli create-test-data basic + +# Datasets need to be assigned to an organisation + +echo "Assigning test Datasets to Organisation open-data-administration-data-requests..." + +curl -LsH "Authorization: ${API_KEY}" \ + --data "id=annakarenina&owner_org=${DR_ORG_ID}" \ + ${CKAN_ACTION_URL}/package_patch + +curl -LsH "Authorization: ${API_KEY}" \ + --data "id=warandpeace&owner_org=${DR_ORG_ID}" \ + ${CKAN_ACTION_URL}/package_patch +## +# END. +# + +ckan_cli search-index rebuild + +if [ "$VENV_DIR" != "" ]; then + deactivate +fi diff --git a/.docker/scripts/doctor.sh b/.docker/scripts/doctor.sh new file mode 100755 index 00000000..d2e81dc8 --- /dev/null +++ b/.docker/scripts/doctor.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# +# Check Drupal-Dev project requirements. +# +set -e + +DOCTOR_CHECK_TOOLS="${DOCTOR_CHECK_TOOLS:-1}" +DOCTOR_CHECK_PORT="${DOCTOR_CHECK_PORT:-0}" +DOCTOR_CHECK_PYGMY="${DOCTOR_CHECK_PYGMY:-1}" +DOCTOR_CHECK_CLI="${DOCTOR_CHECK_CLI:-1}" +DOCTOR_CHECK_SSH="${DOCTOR_CHECK_SSH:-0}" +DOCTOR_CHECK_WEBSERVER="${DOCTOR_CHECK_WEBSERVER:-1}" +DOCTOR_CHECK_BOOTSTRAP="${DOCTOR_CHECK_BOOTSTRAP:-1}" + +APP_PORT="${APP_PORT:-80}" +CLI="${CLI:-cli}" +LAGOON_LOCALDEV_URL="${LAGOON_LOCALDEV_URL:-http://your-site.docker.amazee.io/}" +SSH_KEY_FILE="${SSH_KEY_FILE:-$HOME/.ssh/id_rsa}" +DATAROOT="${DATAROOT:-.data}" + +#------------------------------------------------------------------------------- +# DO NOT CHANGE ANYTHING BELOW THIS LINE +#------------------------------------------------------------------------------- + + +# +# Main entry point. +# +main() { + status "Checking project requirements" + + if [ "${DOCTOR_CHECK_TOOLS}" == "1" ]; then + [ "$(command_exists docker)" == "1" ] && error "Please install Docker (https://www.docker.com/get-started)" && exit 1 + [ "$(command_exists docker-compose)" == "1" ] && error "Please install docker-compose (https://docs.docker.com/compose/install/)" && exit 1 + [ "$(command_exists composer)" == "1" ] && error "Please install Composer (https://getcomposer.org/)" && exit 1 + [ "$(command_exists pygmy)" == "1" ] && error "Please install Pygmy (https://pygmy.readthedocs.io/)" && exit 1 + [ "$(command_exists ahoy)" == "1" ] && error "Please install Ahoy (https://ahoy-cli.readthedocs.io/)" && exit 1 + success "All required tools are present" + fi + + if [ "${DOCTOR_CHECK_PORT}" == "1" ]; then + if ! lsof -i :3000 | grep LISTEN | grep -q om.docke; then + error "Port 3000 is occupied by a service other than Docker. Stop this service and run 'pygmy up'" + fi + success "Port 3000 is available" + fi + + if [ "${DOCTOR_CHECK_PYGMY}" == "1" ]; then + if ! pygmy status > /dev/null 2>&1; then + error "pygmy is not running. Run 'pygmy up' to start pygmy." + exit 1 + fi + success "Pygmy is running" + fi + + # Check that the stack is running. + if [ "${DOCTOR_CHECK_CLI}" == "1" ]; then + if ! docker ps -q --no-trunc | grep "$(docker-compose ps -q ckan)" > /dev/null 2>&1; then + error "CLI container is not running. Run 'ahoy up'." + exit 1 + fi + success "CLI container is running" + fi + + if [ "${DOCTOR_CHECK_SSH}" == "1" ]; then + # SSH key injection is required to access Lagoon services from within + # containers. For example, to connect to production environment to run + # drush script. + # Pygmy makes this possible in the following way: + # 1. Pygmy starts `amazeeio/ssh-agent` container with a volume `/tmp/amazeeio_ssh-agent` + # 2. Pygmy adds a default SSH key from the host into this volume. + # 3. `docker-compose.yml` should have volume inclusion specified for CLI container: + # ``` + # volumes_from: + # - container:amazeeio-ssh-agent + # ``` + # 4. When CLI container starts, the volume is mounted and an entrypoint script + # adds SSH key into agent. + # @see https://github.com/amazeeio/lagoon/blob/master/images/php/cli/10-ssh-agent.sh + # + # Running `ssh-add -L` within CLI container should show that the SSH key + # is correctly loaded. + # + # As rule of a thumb, one must restart the CLI container after restarting + # Pygmy ONLY if SSH key was not loaded in pygmy before the stack starts. + # No need to restart CLI container if key was added, but pygmy was + # restarted - the volume mount will retain and the key will still be + # available in CLI container. + + # Check that the key is injected into pygmy ssh-agent container. + if ! pygmy status 2>&1 | grep -q "${SSH_KEY_FILE}"; then + error "SSH key is not added to pygmy. Run 'pygmy stop && pygmy start' and then 'ahoy up -- --build'." + exit 1 + fi + + # Check that the volume is mounted into CLI container. + if ! docker exec -i "$(docker-compose ps -q ckan)" sh -c "grep \"^/dev\" /etc/mtab|grep -q /tmp/amazeeio_ssh-agent"; then + error "SSH key is added to Pygmy, but the volume is not mounted into container. Make sure that your your \"docker-compose.yml\" has the following lines:" + error "volumes_from:" + error " - container:amazeeio-ssh-agent" + error "After adding these lines, run 'ahoy up -- --build'" + exit 1 + fi + + # Check that ssh key is available in the container. + if ! docker exec -i "$(docker-compose ps -q ckan)" bash -c "ssh-add -L | grep -q 'ssh-rsa'" ; then + error "SSH key was not added into container. Run 'ahoy up -- --build'." + exit 1 + fi + + success "SSH key is available within CLI container" + fi + + + if [ "${DOCTOR_CHECK_WEBSERVER}" == "1" ]; then + host_app_port="$(docker port $(docker-compose ps -q ckan) 3000 | cut -d : -f 2)" + if ! curl -L -s -o /dev/null -w "%{http_code}" "${LAGOON_LOCALDEV_URL}:${host_app_port}" | grep -q 200; then + error "Web server is not accessible at ${LAGOON_LOCALDEV_URL}:${host_app_port}" + exit 1 + fi + success "Web server is running and accessible at ${LAGOON_LOCALDEV_URL}:${host_app_port}" + fi + + if [ "${DOCTOR_CHECK_BOOTSTRAP}" == "1" ]; then + host_app_port="$(docker port $(docker-compose ps -q ckan) 3000 | cut -d : -f 2)" + if ! curl -L -s -N "${LAGOON_LOCALDEV_URL}:${host_app_port}" | grep -q -i "meta name=\"generator\" content=\"ckan"; then + error "Website is running, but cannot be bootstrapped. Try pulling latest container images with 'ahoy pull'" + exit 1 + fi + success "Successfully bootstrapped website at ${LAGOON_LOCALDEV_URL}:${host_app_port}" + fi + + status "All required checks have passed" +} + +# +# Check that command exists. +# +command_exists() { + local cmd=$1 + command -v "${cmd}" | grep -ohq "${cmd}" + local res=$? + + # Try homebrew lookup, if brew is available. + if command -v "brew" | grep -ohq "brew" && [ "$res" == "1" ] ; then + brew --prefix "${cmd}" > /dev/null + res=$? + fi + + echo ${res} +} + +# +# Status echo. +# +status() { + cecho blue "✚ $1"; +} + +# +# Success echo. +# +success() { + cecho green " ✓ $1"; +} + +# +# Error echo. +# +error() { + cecho red " ✘ $1"; + exit 1 +} + +# +# Colored echo. +# +cecho() { + local prefix="\033[" + local input_color=$1 + local message="$2" + + local color="" + case "$input_color" in + black | bk) color="${prefix}0;30m";; + red | r) color="${prefix}1;31m";; + green | g) color="${prefix}1;32m";; + yellow | y) color="${prefix}1;33m";; + blue | b) color="${prefix}1;34m";; + purple | p) color="${prefix}1;35m";; + cyan | c) color="${prefix}1;36m";; + gray | gr) color="${prefix}0;37m";; + *) message="$1" + esac + + # Format message with color codes, but only if a correct color was provided. + [ -n "$color" ] && message="${color}${message}${prefix}0m" + + echo -e "$message" +} + +main "$@" diff --git a/.docker/scripts/init-ext.sh b/.docker/scripts/init-ext.sh new file mode 100755 index 00000000..07be8fe1 --- /dev/null +++ b/.docker/scripts/init-ext.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env sh +## +# Install current extension. +# +set -e + +if [ "$VENV_DIR" != "" ]; then + . ${VENV_DIR}/bin/activate +fi +pip install -r "requirements.txt" +pip install -r "requirements-dev.txt" +python setup.py develop +installed_name=$(grep '^\s*name=' setup.py |sed "s|[^']*'\([-a-zA-Z0-9]*\)'.*|\1|") + +# Validate that the extension was installed correctly. +if ! pip list | grep "$installed_name" > /dev/null; then echo "Unable to find the extension in the list"; exit 1; fi + +if [ "$VENV_DIR" != "" ]; then + deactivate +fi diff --git a/.docker/scripts/init.sh b/.docker/scripts/init.sh new file mode 100755 index 00000000..959d7426 --- /dev/null +++ b/.docker/scripts/init.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh +## +# Initialise CKAN data for testing. +# +set -e + +if [ "$VENV_DIR" != "" ]; then + . ${VENV_DIR}/bin/activate +fi +CLICK_ARGS="--yes" ckan_cli db clean +ckan_cli db init +ckan_cli db upgrade + +CKAN_USER_NAME="${CKAN_USER_NAME:-admin}" +CKAN_DISPLAY_NAME="${CKAN_DISPLAY_NAME:-Administrator}" +CKAN_USER_EMAIL="${CKAN_USER_EMAIL:-admin@localhost}" +CKAN_USER_PASSWORD="${CKAN_USER_PASSWORD:-Password123!}" +ckan_cli user add "$CKAN_USER_NAME" fullname="$CKAN_DISPLAY_NAME" email="$CKAN_USER_EMAIL" password="$CKAN_USER_PASSWORD" + +# Create some base test data +. $APP_DIR/scripts/create-test-data.sh diff --git a/.docker/scripts/serve.sh b/.docker/scripts/serve.sh new file mode 100755 index 00000000..10f3dead --- /dev/null +++ b/.docker/scripts/serve.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env sh +set -e + +dockerize -wait tcp://postgres:5432 -timeout 1m +dockerize -wait tcp://solr:8983 -timeout 1m +dockerize -wait tcp://redis:6379 -timeout 1m + +sed -i "s@SITE_URL@${SITE_URL}@g" $CKAN_INI + +if [ "$VENV_DIR" != "" ]; then + . ${VENV_DIR}/bin/activate +fi +if (which ckan > /dev/null); then + ckan -c ${CKAN_INI} run +else + paster serve ${CKAN_INI} +fi diff --git a/.docker/test.ini b/.docker/test.ini new file mode 100644 index 00000000..62562044 --- /dev/null +++ b/.docker/test.ini @@ -0,0 +1,213 @@ +# +# CKAN - Pylons configuration +# +# These are some of the configuration options available for your CKAN +# instance. Check the documentation in 'doc/configuration.rst' or at the +# following URL for a description of what they do and the full list of +# available options: +# +# http://docs.ckan.org/en/latest/maintaining/configuration.html +# +# The %(here)s variable will be replaced with the parent directory of this file +# + +[DEFAULT] +debug = false +smtp_server = localhost +error_email_from = paste@localhost + +[server:main] +use = egg:Paste#http +host = 0.0.0.0 +port = 3000 + +[app:main] +ckan.devserver.host = 0.0.0.0 +ckan.devserver.port = 3000 + +use = egg:ckan +full_stack = true +cache_dir = /tmp/%(ckan.site_id)s/ + +# This is the secret token that the beaker library uses to hash the cookie sent +# to the client. `paster make-config` generates a unique value for this each +# time it generates a config file. +beaker.session.secret = bSmgPpaxg2M+ZRes3u1TXwIcE + +# `paster make-config` generates a unique value for this each time it generates +# a config file. +app_instance_uuid = 6e3daf8e-1c6b-443b-911f-c7ab4c5f9605 + +who.config_file = %(here)s/who.ini +who.log_level = warning +who.log_file = %(cache_dir)s/who_log.ini + +## Database Settings +sqlalchemy.url = postgresql://ckan:ckan@postgres/ckan?sslmode=disable + +ckan.datastore.write_url = postgresql://ckan:ckan@postgres-datastore/ckan?sslmode=disable +ckan.datastore.read_url = postgresql://ckan_datastore:ckan@postgres-datastore/ckan?sslmode=disable + +# PostgreSQL' full-text search parameters +ckan.datastore.default_fts_lang = english +ckan.datastore.default_fts_index_method = gist + +## Site Settings. +ckan.site_url = http://ckan:3000/ + +## Authorization Settings + +ckan.auth.anon_create_dataset = false +ckan.auth.create_unowned_dataset = false +ckan.auth.create_dataset_if_not_in_organization = false +ckan.auth.user_create_groups = false +ckan.auth.user_create_organizations = false +ckan.auth.user_delete_groups = true +ckan.auth.user_delete_organizations = true +ckan.auth.create_user_via_api = false +ckan.auth.create_user_via_web = true +ckan.auth.roles_that_cascade_to_sub_groups = admin +ckan.auth.public_user_details = False + + +## Search Settings + +ckan.site_id = default +solr_url = http://solr:8983/solr/ckan + + +## Redis Settings + +# URL to your Redis instance, including the database to be used. +ckan.redis.url = redis://redis:6379 + + +## CORS Settings + +# If cors.origin_allow_all is true, all origins are allowed. +# If false, the cors.origin_whitelist is used. +# ckan.cors.origin_allow_all = true +# cors.origin_whitelist is a space separated list of allowed domains. +# ckan.cors.origin_whitelist = http://example1.com http://example2.com + + +## Plugins Settings + +# Note: Add ``datastore`` to enable the CKAN DataStore +# Add ``datapusher`` to enable DataPusher +# Add ``resource_proxy`` to enable resource proxying and get around the +# same origin policy +ckan.plugins = stats text_view image_view recline_view datastore datarequests + +# Define which views should be created by default +# (plugins must be loaded in ckan.plugins) +ckan.views.default_views = image_view text_view recline_view + +# Customize which text formats the text_view plugin will show +#ckan.preview.json_formats = json +#ckan.preview.xml_formats = xml rdf rdf+xml owl+xml atom rss +#ckan.preview.text_formats = text plain text/plain + +# Customize which image formats the image_view plugin will show +#ckan.preview.image_formats = png jpeg jpg gif + +## Internationalisation Settings +ckan.locale_default = en +ckan.locale_order = en pt_BR ja it cs_CZ ca es fr el sv sr sr@latin no sk fi ru de pl nl bg ko_KR hu sa sl lv +ckan.locales_offered = +ckan.locales_filtered_out = en_GB + +## Feeds Settings + +ckan.feeds.authority_name = +ckan.feeds.date = +ckan.feeds.author_name = +ckan.feeds.author_link = + +## Storage Settings + +ckan.storage_path = /app/filestore +#ckan.max_resource_size = 10 +#ckan.max_image_size = 2 + +## Datapusher settings + +# Make sure you have set up the DataStore + +#ckan.datapusher.formats = csv xls xlsx tsv application/csv application/vnd.ms-excel application/vnd.openxmlformats-officedocument.spreadsheetml.sheet +#ckan.datapusher.url = http://127.0.0.1:8800/ +#ckan.datapusher.assume_task_stale_after = 3600 + +# Resource Proxy settings +# Preview size limit, default: 1MB +#ckan.resource_proxy.max_file_size = 1048576 +# Size of chunks to read/write. +#ckan.resource_proxy.chunk_size = 4096 + +## Activity Streams Settings + +#ckan.activity_streams_enabled = true +#ckan.activity_list_limit = 31 +#ckan.activity_streams_email_notifications = true +#ckan.email_notifications_since = 2 days +ckan.hide_activity_from_users = %(ckan.site_id)s + + +## Email settings +# If 'smtp.test_server' is configured we assume we're running tests, +# and don't use the smtp.server, starttls, user, password etc. options. +smtp.test_server = localhost:8025 +smtp.mail_from = info@test.ckan.net + +## Harvester settings +ckan.harvest.mq.type = redis +ckan.harvest.mq.hostname = redis +ckan.harvest.mq.port = 6379 +ckan.harvest.mq.redis_db = 0 + +## ckanext-datarequests settings +# Enable or disable the comments system by setting up the ckan.datarequests.comments property in the configuration file (by default, the comments system is enabled). +ckan.datarequests.comments = true +# Enable or disable a badge to show the number of data requests in the menu by setting up the ckan.datarequests.show_datarequests_badge property in the configuration file (by default, the badge is not shown). +ckan.datarequests.show_datarequests_badge = true +# Enable or disable description as a required field on data request forms +ckan.datarequests.description_required = true +# Default organisation used for new data requests +ckan.datarequests.default_organisation = open-data-administration-data-requests +# Enable or disable circumstances for closing data requests. Default value is False +ckan.datarequests.enable_closing_circumstances = True + +## Logging configuration +[loggers] +keys = root, ckan, ckanext + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console + +[logger_ckan] +level = INFO +handlers = console +qualname = ckan +propagate = 0 + +[logger_ckanext] +level = DEBUG +handlers = console +qualname = ckanext +propagate = 0 + +[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/.env b/.env new file mode 100644 index 00000000..8cc7b5bd --- /dev/null +++ b/.env @@ -0,0 +1,27 @@ +## +# Project environment variables. +# +# It is used by Ahoy and other scripts to read default values. +# +# The values must be scalar (cannot be another variable). +# +# You may also create .env.local file to override any values locally (it is +# excluded from git). +# + +# Project name. +PROJECT="ckanext-datarequests" + +# Docker Compose project name. All containers will have this name. +COMPOSE_PROJECT_NAME="$PROJECT" + +# Flag to allow code linting failures. 0=enforce, 1=ignore +ALLOW_LINT_FAIL=0 + +# Flag to allow unit tests failures. 0=enforce, 1=ignore +ALLOW_UNIT_FAIL=0 + +# Flag to allow BDD tests failures. 0=enforce, 1=ignore +ALLOW_BDD_FAIL=0 + +CKAN_REPO=ckan/ckan diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..9653307b --- /dev/null +++ b/.flake8 @@ -0,0 +1,22 @@ +[flake8] +# @see https://flake8.pycqa.org/en/latest/user/configuration.html?highlight=.flake8 + +exclude = + ckan + scripts + +# Extended output format. +format = pylint + +# Show the source of errors. +show_source = True + +max-complexity = 10 + +# List ignore rules one per line. +ignore = + E241 + E266 + E501 + C901 + W503 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..aee1b377 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Ignore files for distribution archives. +/.ahoy.yml export-ignore +/.circleci export-ignore +/.docker export-ignore +/.env export-ignore +/.gitatributes export-ignore +/docker-compose.yml export-ignore +/test export-ignore diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..b5158981 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +registries: + python-index-pypi-org: + type: python-index + url: https://pypi.org/ + replaces-base: true + username: "${{secrets.PYTHON_INDEX_PYPI_ORG_USERNAME}}" + password: "${{secrets.PYTHON_INDEX_PYPI_ORG_PASSWORD}}" + +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: daily + time: "19:00" + open-pull-requests-limit: 10 + registries: + - python-index-pypi-org diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..ece09000 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,46 @@ +--- +name: Tests +on: + push: + pull_request: + branches: + - master + +jobs: + test: + strategy: + fail-fast: false + matrix: + ckan-version: [2.8.8, 2.9.5] + + name: Continuous Integration build on CKAN ${{ matrix.ckan-version }} + runs-on: ubuntu-latest + container: drevops/ci-builder + env: + CKAN_REPO: ckan/ckan + CKAN_VERSION: ${{ matrix.ckan-version }} + + steps: + - uses: actions/checkout@v2 + timeout-minutes: 2 + + - name: Build + run: .circleci/build.sh + timeout-minutes: 10 + + - name: Test + run: .circleci/test.sh + timeout-minutes: 15 + + - name: Retrieve screenshots + if: failure() + run: .circleci/process-artifacts.sh + timeout-minutes: 1 + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v2 + with: + name: CKAN ${{ matrix.ckan-version }} screenshots + path: /tmp/artifacts/behave/screenshots + timeout-minutes: 1 diff --git a/.gitignore b/.gitignore index 55c0f302..4578f100 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +.ropeproject +node_modules +bower_components + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -5,27 +9,17 @@ __pycache__/ # C extensions *.so -### OSX Stuff -*.DS_Store -.AppleDouble -.LSOverride - # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ -downloads/ -eggs/ -lib/ -lib64/ -parts/ sdist/ -var/ *.egg-info/ .installed.cfg *.egg +*.eggs # PyInstaller # Usually these files are written by a python script from a template @@ -54,6 +48,7 @@ coverage.xml # Sphinx documentation docs/_build/ - -# PyBuilder -target/ +.env.local +screenshots +!/test/screenshots/.gitkeep +.idea diff --git a/.travis.yml b/.travis.yml index cf7ef23a..ae69279b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,17 +7,17 @@ env: - CKANVERSION=2.5.6 POSTGISVERSION=2 - CKANVERSION=2.6.3 POSTGISVERSION=2 - CKANVERSION=2.7.0 POSTGISVERSION=2 - - CKANVERSION=2.8.1 POSTGISVERSION=2 + - CKANVERSION=2.8.3 POSTGISVERSION=2 services: - redis-server - postgresql + - xvfb addons: firefox: "46.0" install: - bash bin/travis-build.bash before_script: - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" - sleep 3 # give xvfb some time to start script: - sh bin/travis-run.sh diff --git a/README.md b/README.md index ce42effb..a31b86a8 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ In addition, you should note that the parameters will be checked and an exceptio ##### Returns: A dict with the data request (`id`, `user_id`, `title`, `description`,`organization_id`, `open_time`, `accepted_dataset`, `close_time`, `closed`, `followers`). - #### `show_datarequest(context, data_dict)` Action to retrieve the information of a data request. The only required parameter is the `id` of the data request. A `NotFound` exception will be risen if the `id` is not found. @@ -51,6 +50,85 @@ Access rights will be checked before returning the information and an exception A dict with the data request (`id`, `user_id`, `title`, `description`,`organization_id`, `open_time`, `accepted_dataset`, `close_time`, `closed`, `followers`). +## Local environment setup +- Make sure that you have latest versions of all required software installed: + - [Docker](https://www.docker.com/) + - [Pygmy](https://pygmy.readthedocs.io/) + - [Ahoy](https://github.com/ahoy-cli/ahoy) +- Make sure that all local web development services are shut down (Apache/Nginx, Mysql, MAMP etc). +- Checkout project repository (in one of the [supported Docker directories](https://docs.docker.com/docker-for-mac/osxfs/#access-control)). +- `pygmy up` +- `ahoy build` + +Use `admin`/`password` to login to CKAN. + +## Available `ahoy` commands +Run each command as `ahoy `. + ``` + build Build or rebuild project. + clean Remove containers and all build files. + cli Start a shell inside CLI container or run a command. + doctor Find problems with current project setup. + down Stop Docker containers and remove container, images, volumes and networks. + flush-redis Flush Redis cache. + info Print information about this project. + install-site Install a site. + lint Lint code. + logs Show Docker logs. + pull Pull latest docker images. + reset Reset environment: remove containers, all build, manually created and Drupal-Dev files. + restart Restart all stopped and running Docker containers. + start Start existing Docker containers. + stop Stop running Docker containers. + test-bdd Run BDD tests. + test-unit Run unit tests. + up Build and start Docker containers. + ``` + +## Coding standards +Python code linting uses [flake8](https://github.com/PyCQA/flake8) with configuration captured in `.flake8` file. + +Set `ALLOW_LINT_FAIL=1` in `.env` to allow lint failures. + +## Nose tests +`ahoy test-unit` + +Set `ALLOW_UNIT_FAIL=1` in `.env` to allow unit test failures. + +## Behavioral tests +`ahoy test-bdd` + +Set `ALLOW_BDD_FAIL=1` in `.env` to allow BDD test failures. + +### How it works +We are using [Behave](https://github.com/behave/behave) BDD _framework_ with additional _step definitions_ provided by [Behaving](https://github.com/ggozad/behaving) library. + +Custom steps described in `test/features/steps/steps.py`. + +Test scenarios located in `test/features/*.feature` files. + +Test environment configuration is located in `test/features/environment.py` and is setup to connect to a remote Chrome +instance running in a separate Docker container. + +During the test, Behaving passes connection information to [Splinter](https://github.com/cobrateam/splinter) which +instantiates WebDriver object and establishes connection with Chrome instance. All further communications with Chrome +are handled through this driver, but in a developer-friendly way. + +For a list of supported step-definitions, see https://github.com/ggozad/behaving#behavingweb-supported-matcherssteps. + +## Automated builds (Continuous Integration) +In software engineering, continuous integration (CI) is the practice of merging all developer working copies to a shared mainline several times a day. +Before feature changes can be merged into a shared mainline, a complete build must run and pass all tests on CI server. + +This project uses [Circle CI](https://circleci.com/) as a CI server: it imports production backups into fully built codebase and runs code linting and tests. When tests pass, a deployment process is triggered for nominated branches (usually, `master` and `develop`). + +Add `[skip ci]` to the commit subject to skip CI build. Useful for documentation changes. + +### SSH +Circle CI supports shell access to the build for 120 minutes after the build is finished when the build is started with SSH support. Use "Rerun job with SSH" button in Circle CI UI to start build with SSH support. + +## Actions + #### `update_datarequest(context, data_dict)` Action to update a data request. The function checks the access rights of the user before updating the data request. If the user is not allowed, a `NotAuthorized` exception will be risen @@ -178,7 +256,7 @@ Action to unfollow a data request. Access rights will be cheked before unfollowi ## Installation -Install this extension in your CKAN instance is as easy as install any other CKAN extension. +Install this extension in your CKAN instance is as easy as installing any other CKAN extension. * Activate your virtual environment ``` @@ -186,13 +264,11 @@ Install this extension in your CKAN instance is as easy as install any other CKA ``` * Install the extension ``` -pip install ckanext-datarequests +pip install 'git+https://github.com/conwetlab/ckanext-datarequests.git#egg=ckanext-datarequests' ``` -> **Note**: If you prefer, you can also download the source code and install the extension manually. To do so, execute the following commands: +> **Note**: If you prefer, you can download the source code as well and install in 'develop' mode for easy editing. To do so, use the '-e' flag: > ``` -> $ git clone https://github.com/conwetlab/ckanext-datarequests.git -> $ cd ckanext-datarequests -> $ python setup.py install +> pip install -e 'git+https://github.com/conwetlab/ckanext-datarequests.git#egg=ckanext-datarequests' > ``` * Modify your configuration file (generally in `/etc/ckan/default/production.ini`) and add `datarequests` in the `ckan.plugins` property. @@ -207,6 +283,10 @@ ckan.datarequests.comments = [true|false] ``` ckan.datarequests.show_datarequests_badge = [true|false] ``` +* Enable or disable description as a required field on data request forms. False by default +``` +ckan.datarequests.description_required = [True|False] +``` * Restart your apache2 reserver ``` sudo service apache2 restart @@ -215,7 +295,7 @@ sudo service apache2 restart ## Translations -Help us to translate this extension so everyone can create data requests. Currently, the extension is translated to English, Spanish, German and Brazilian Portuguese. If you want to contribute with your translation, the first step is to clone this repo and move to the `develop` branch. Then, create the locale for your translation by executing: +Help us to translate this extension so everyone can create data requests. Currently, the extension is translated to English, Spanish, German, French, Somali, Romanian and Brazilian Portuguese. If you want to contribute with your translation, the first step is to clone this repo and move to the `develop` branch. Then, create the locale for your translation by executing: ``` python setup.py init_catalog -l @@ -234,7 +314,7 @@ Once the translation files (`po`) have been updated, compile them by running: python setup.py compile_catalog ``` -This will generate the required `mo` file. Once this file has been generated, commit your changes and create a Pull Request (to the `develop` branch). +This will generate the required `mo` file. Once this file has been generated, commit your changes and create a Pull Request (to the `develop` branch). ## Tests @@ -248,6 +328,14 @@ python setup.py nosetests ## Changelog +### v1.2.0 (UNRELEASED) + +* New: French translations (thanks to @bobeal) +* New: Romanian translations (thanks to @costibleotu) +* New: Option to force users to introduce a request description (thanks to @MarkCalvert) +* Fix: Documentation fixes (thanks to @nykc) +* Fix: Datarequests creation and closing times displayed incorrectly (thanks to @iamarnavgarg) + ### v1.1.0 * New: Compatibility with CKAN 2.8.0 @@ -285,4 +373,3 @@ python setup.py nosetests ### v0.3.3 * New: German Translation (thanks to @kvlahrosch) - diff --git a/bin/travis-build.bash b/bin/travis-build.bash index c4325520..fdbc3548 100644 --- a/bin/travis-build.bash +++ b/bin/travis-build.bash @@ -12,8 +12,8 @@ git clone https://github.com/ckan/ckan cd ckan git checkout ckan-$CKANVERSION python setup.py develop -pip install -r requirements.txt --allow-all-external -pip install -r dev-requirements.txt --allow-all-external +pip install -r requirements.txt +pip install -r dev-requirements.txt cd - echo "Setting up Solr..." @@ -21,9 +21,9 @@ echo "Setting up Solr..." # on Travis single-core still. # see https://github.com/ckan/ckan/issues/2972 sed -i -e 's/solr_url.*/solr_url = http:\/\/127.0.0.1:8983\/solr/' ckan/test-core.ini -printf "NO_START=0\nJETTY_HOST=127.0.0.1\nJETTY_PORT=8983\nJAVA_HOME=$JAVA_HOME" | sudo tee /etc/default/jetty +printf "NO_START=0\nJETTY_HOST=127.0.0.1\nJETTY_PORT=8983\nJAVA_HOME=$JAVA_HOME" | sudo tee /etc/default/jetty8 sudo cp ckan/ckan/config/solr/schema.xml /etc/solr/conf/schema.xml -sudo service jetty restart +sudo service jetty8 restart echo "Creating the PostgreSQL user and database..." sudo -u postgres psql -c "CREATE USER ckan_default WITH PASSWORD 'pass';" @@ -40,4 +40,4 @@ cd - echo "Installing ckanext-datarequests and its requirements..." python setup.py develop -echo "travis-build.bash is done." \ No newline at end of file +echo "travis-build.bash is done." diff --git a/bin/travis-run.sh b/bin/travis-run.sh deleted file mode 100644 index 8bc0c729..00000000 --- a/bin/travis-run.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -e - -python setup.py nosetests diff --git a/ckanext/datarequests/actions.py b/ckanext/datarequests/actions.py index f251084a..a95fdd3a 100644 --- a/ckanext/datarequests/actions.py +++ b/ckanext/datarequests/actions.py @@ -18,22 +18,19 @@ # along with CKAN Data Requests Extension. If not, see . -import ckan.lib.base as base -import ckan.model as model -import ckan.plugins as plugins -import constants import datetime import cgi -import db import logging -import validator -import ckan.lib.mailer as mailer -from pylons import config +from ckan import model +from ckan.lib import mailer +from ckan.plugins import toolkit as tk +from ckan.plugins.toolkit import h, config + +from . import constants, db, validator + -c = plugins.toolkit.c log = logging.getLogger(__name__) -tk = plugins.toolkit # Avoid user_show lag USERS_CACHE = {} @@ -54,7 +51,7 @@ def _get_user(user_id): def _get_organization(organization_id): try: organization_show = tk.get_action('organization_show') - return organization_show({'ignore_auth': True}, {'id': organization_id}) + return organization_show({'ignore_auth': True}, {'id': organization_id, 'include_users': True}) except Exception as e: log.warn(e) @@ -101,14 +98,25 @@ def _dictize_datarequest(datarequest): data_dict['followers'] = db.DataRequestFollower.get_datarequest_followers_number( datarequest_id=datarequest.id) + if h.closing_circumstances_enabled: + data_dict['close_circumstance'] = datarequest.close_circumstance + data_dict['approx_publishing_date'] = datarequest.approx_publishing_date + return data_dict -def _undictize_datarequest_basic(data_request, data_dict): - data_request.title = data_dict['title'] - data_request.description = data_dict['description'] +def _undictize_datarequest_basic(datarequest, data_dict): + datarequest.title = data_dict['title'] + datarequest.description = data_dict['description'] organization = data_dict['organization_id'] - data_request.organization_id = organization if organization else None + datarequest.organization_id = organization if organization else None + _undictize_datarequest_closing_circumstances(datarequest, data_dict) + + +def _undictize_datarequest_closing_circumstances(datarequest, data_dict): + if h.closing_circumstances_enabled: + datarequest.close_circumstance = data_dict.get('close_circumstance') or None + datarequest.approx_publishing_date = data_dict.get('approx_publishing_date') or None def _dictize_comment(comment): @@ -131,7 +139,7 @@ def _undictize_comment_basic(comment, data_dict): def _get_datarequest_involved_users(context, datarequest_dict): datarequest_id = datarequest_dict['id'] - new_context = {'ignore_auth': True, 'model': context['model'] } + new_context = {'ignore_auth': True, 'model': context['model']} # Creator + Followers + People who has commented + Organization Staff users = set() @@ -141,7 +149,7 @@ def _get_datarequest_involved_users(context, datarequest_dict): if datarequest_dict['organization']: users.update([user['id'] for user in datarequest_dict['organization']['users']]) - + # Notifications are not sent to the user that performs the action users.discard(context['auth_user_obj'].id) @@ -160,13 +168,13 @@ def _send_mail(user_ids, action_type, datarequest): 'site_url': config.get('ckan.site_url') } - subject = base.render_jinja2('emails/subjects/{0}.txt'.format(action_type), extra_vars) - body = base.render_jinja2('emails/bodies/{0}.txt'.format(action_type), extra_vars) + subject = tk.render('emails/subjects/{0}.txt'.format(action_type), extra_vars) + body = tk.render('emails/bodies/{0}.txt'.format(action_type), extra_vars) mailer.mail_user(user_data, subject, body) except Exception: - logging.exception("Error sending notification to {0}".format(user_id)) + log.exception("Error sending notification to {0}".format(user_id)) def create_datarequest(context, data_dict): @@ -190,7 +198,7 @@ def create_datarequest(context, data_dict): :type organization_id: string :returns: A dict with the data request (id, user_id, title, description, - organization_id, open_time, accepted_dataset, close_time, closed, + organization_id, open_time, accepted_dataset, close_time, closed, followers) :rtype: dict ''' @@ -211,15 +219,15 @@ def create_datarequest(context, data_dict): data_req = db.DataRequest() _undictize_datarequest_basic(data_req, data_dict) data_req.user_id = context['auth_user_obj'].id - data_req.open_time = datetime.datetime.now() + data_req.open_time = datetime.datetime.utcnow() session.add(data_req) - session.commit() + session.commit() datarequest_dict = _dictize_datarequest(data_req) if datarequest_dict['organization']: - users = set([user['id'] for user in datarequest_dict['organization']['users']]) + users = {user['id'] for user in datarequest_dict['organization']['users']} users.discard(context['auth_user_obj'].id) _send_mail(users, 'new_datarequest', datarequest_dict) @@ -239,7 +247,7 @@ def show_datarequest(context, data_dict): :type id: string :returns: A dict with the data request (id, user_id, title, description, - organization_id, open_time, accepted_dataset, close_time, closed, + organization_id, open_time, accepted_dataset, close_time, closed, followers) :rtype: dict ''' @@ -291,7 +299,7 @@ def update_datarequest(context, data_dict): :type organization_id: string :returns: A dict with the data request (id, user_id, title, description, - organization_id, open_time, accepted_dataset, close_time, closed, + organization_id, open_time, accepted_dataset, close_time, closed, followers) :rtype: dict ''' @@ -444,7 +452,7 @@ def list_datarequests(context, data_dict): 'display_name': organization.get('display_name'), 'count': no_processed_organization_facet[organization_id] }) - except: + except Exception: pass state_facet = [] @@ -482,7 +490,7 @@ def delete_datarequest(context, data_dict): :type id: string :returns: A dict with the data request (id, user_id, title, description, - organization_id, open_time, accepted_dataset, close_time, closed, + organization_id, open_time, accepted_dataset, close_time, closed, followers) :rtype: dict ''' @@ -527,7 +535,7 @@ def close_datarequest(context, data_dict): :type accepted_dataset_id: string :returns: A dict with the data request (id, user_id, title, description, - organization_id, open_time, accepted_dataset, close_time, closed, + organization_id, open_time, accepted_dataset, close_time, closed, followers) :rtype: dict @@ -562,8 +570,9 @@ def close_datarequest(context, data_dict): raise tk.ValidationError([tk._('This Data Request is already closed')]) data_req.closed = True - data_req.accepted_dataset_id = data_dict.get('accepted_dataset_id', None) - data_req.close_time = datetime.datetime.now() + data_req.accepted_dataset_id = data_dict.get('accepted_dataset_id') or None + data_req.close_time = datetime.datetime.utcnow() + _undictize_datarequest_closing_circumstances(data_req, data_dict) session.add(data_req) session.commit() @@ -616,7 +625,7 @@ def comment_datarequest(context, data_dict): comment = db.Comment() _undictize_comment_basic(comment, data_dict) comment.user_id = context['auth_user_obj'].id - comment.time = datetime.datetime.now() + comment.time = datetime.datetime.utcnow() session.add(comment) session.commit() @@ -806,13 +815,14 @@ def delete_datarequest_comment(context, data_dict): return _dictize_comment(comment) + def follow_datarequest(context, data_dict): ''' - Action to follow a data request. Access rights will be cheked before + Action to follow a data request. Access rights will be cheked before following a datarequest and a NotAuthorized exception will be risen if the user is not allowed to follow the given datarequest. ValidationError will be risen if the datarequest ID is not included or if the user is already - following the datarequest. ObjectNotFound will be risen if the given + following the datarequest. ObjectNotFound will be risen if the given datarequest does not exist. :param id: The ID of the datarequest to be followed @@ -857,13 +867,14 @@ def follow_datarequest(context, data_dict): return True + def unfollow_datarequest(context, data_dict): ''' - Action to unfollow a data request. Access rights will be cheked before + Action to unfollow a data request. Access rights will be cheked before unfollowing a datarequest and a NotAuthorized exception will be risen if the user is not allowed to unfollow the given datarequest. ValidationError - will be risen if the datarequest ID is not included in the request. - ObjectNotFound will be risen if the user is not following the given + will be risen if the datarequest ID is not included in the request. + ObjectNotFound will be risen if the user is not following the given datarequest. :param id: The ID of the datarequest to be unfollowed diff --git a/ckanext/datarequests/auth.py b/ckanext/datarequests/auth.py index ed231fc6..5bfa7c24 100644 --- a/ckanext/datarequests/auth.py +++ b/ckanext/datarequests/auth.py @@ -17,15 +17,16 @@ # You should have received a copy of the GNU Affero General Public License # along with CKAN Data Requests Extension. If not, see . -import constants -from ckan.plugins import toolkit as tk +from ckan.plugins.toolkit import auth_allow_anonymous_access, get_action + +from . import constants def create_datarequest(context, data_dict): return {'success': True} -@tk.auth_allow_anonymous_access +@auth_allow_anonymous_access def show_datarequest(context, data_dict): return {'success': True} @@ -33,7 +34,7 @@ def show_datarequest(context, data_dict): def auth_if_creator(context, data_dict, show_function): # Sometimes data_dict only contains the 'id' if 'user_id' not in data_dict: - function = tk.get_action(show_function) + function = get_action(show_function) data_dict = function({'ignore_auth': True}, {'id': data_dict.get('id')}) return {'success': data_dict['user_id'] == context.get('auth_user_obj').id} @@ -43,7 +44,7 @@ def update_datarequest(context, data_dict): return auth_if_creator(context, data_dict, constants.SHOW_DATAREQUEST) -@tk.auth_allow_anonymous_access +@auth_allow_anonymous_access def list_datarequests(context, data_dict): return {'success': True} @@ -60,13 +61,13 @@ def comment_datarequest(context, data_dict): return {'success': True} -@tk.auth_allow_anonymous_access +@auth_allow_anonymous_access def list_datarequest_comments(context, data_dict): new_data_dict = {'id': data_dict['datarequest_id']} return show_datarequest(context, new_data_dict) -@tk.auth_allow_anonymous_access +@auth_allow_anonymous_access def show_datarequest_comment(context, data_dict): return {'success': True} diff --git a/ckanext/datarequests/common.py b/ckanext/datarequests/common.py new file mode 100644 index 00000000..997a4bcd --- /dev/null +++ b/ckanext/datarequests/common.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 Queensland Government + +# This file is part of CKAN Data Requests Extension. + +# CKAN Data Requests Extension 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. + +# CKAN Data Requests Extension 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 CKAN Data Requests Extension. If not, see . + +from ckan.common import config +import ckan.lib.helpers as h + + +def get_config_bool_value(config_name, default_value=False): + value = config.get(config_name, default_value) + value = value if type(value) == bool else value != 'False' + return value + + +def is_fontawesome_4(): + if hasattr(h, 'ckan_version'): + ckan_version = float(h.ckan_version()[0:3]) + return ckan_version >= 2.7 + else: + return False + + +def get_plus_icon(): + return 'plus-square' if is_fontawesome_4() else 'plus-sign-alt' + + +def get_question_icon(): + return 'question-circle' if is_fontawesome_4() else 'question-sign' diff --git a/ckanext/datarequests/constants.py b/ckanext/datarequests/constants.py index 560ed748..4f726818 100644 --- a/ckanext/datarequests/constants.py +++ b/ckanext/datarequests/constants.py @@ -35,3 +35,4 @@ DESCRIPTION_MAX_LENGTH = 1000 COMMENT_MAX_LENGTH = DESCRIPTION_MAX_LENGTH DATAREQUESTS_PER_PAGE = 10 +CLOSE_CIRCUMSTANCE_MAX_LENGTH = 255 diff --git a/ckanext/datarequests/controllers/controller_functions.py b/ckanext/datarequests/controllers/controller_functions.py new file mode 100644 index 00000000..ce158dec --- /dev/null +++ b/ckanext/datarequests/controllers/controller_functions.py @@ -0,0 +1,426 @@ +# encoding: utf-8 + +import functools +import logging +import re +import six + +from six.moves.urllib.parse import urlencode + +from ckan import model +from ckan.lib import helpers +from ckan.plugins import toolkit as tk +from ckan.plugins.toolkit import c, h + +from ckanext.datarequests import constants, request_helpers + + +_link = re.compile(r'(?:(https?://)|(www\.))(\S+\b/?)([!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}~]*)(\s|$)', re.I) + +log = logging.getLogger(__name__) + + +def _get_errors_summary(errors): + errors_summary = {} + + for key, error in errors.items(): + errors_summary[key] = ', '.join(error) + + return errors_summary + + +def _encode_params(params): + return [(k, v.encode('utf-8') if isinstance(v, six.string_types) else str(v)) + for k, v in params] + + +def url_with_params(url, params): + params = _encode_params(params) + return url + u'?' + urlencode(params) + + +def search_url(params): + url = tk.url_for('datarequest.index') + return url_with_params(url, params) + + +def org_datarequest_url(params, id): + url = tk.url_for('datarequest.organization', id=id) + return url_with_params(url, params) + + +def user_datarequest_url(params, id): + url = tk.url_for('datarequest.user', id=id) + return url_with_params(url, params) + + +def _get_context(): + return {'model': model, 'session': model.Session, + 'user': c.user, 'auth_user_obj': c.userobj} + + +def _show_index(user_id, organization_id, include_organization_facet, url_func, file_to_render): + + def pager_url(state=None, sort=None, q=None, page=None): + params = [] + + if q: + params.append(('q', q)) + + if state is not None: + params.append(('state', state)) + + params.append(('sort', sort)) + params.append(('page', page)) + + return url_func(params) + + try: + context = _get_context() + page = int(request_helpers.get_first_query_param('page', 1)) + limit = constants.DATAREQUESTS_PER_PAGE + offset = (page - 1) * constants.DATAREQUESTS_PER_PAGE + data_dict = {'offset': offset, 'limit': limit} + + state = request_helpers.get_first_query_param('state', None) + if state: + data_dict['closed'] = True if state == 'closed' else False + + q = request_helpers.get_first_query_param('q', '') + if q: + data_dict['q'] = q + + if organization_id: + data_dict['organization_id'] = organization_id + + if user_id: + data_dict['user_id'] = user_id + + sort = request_helpers.get_first_query_param('sort', 'desc') + sort = sort if sort in ['asc', 'desc'] else 'desc' + if sort is not None: + data_dict['sort'] = sort + + tk.check_access(constants.LIST_DATAREQUESTS, context, data_dict) + datarequests_list = tk.get_action(constants.LIST_DATAREQUESTS)(context, data_dict) + + c.filters = [(tk._('Newest'), 'desc'), (tk._('Oldest'), 'asc')] + c.sort = sort + c.q = q + c.organization = organization_id + c.state = state + c.datarequest_count = datarequests_list['count'] + c.datarequests = datarequests_list['result'] + c.search_facets = datarequests_list['facets'] + c.page = helpers.Page( + collection=datarequests_list['result'], + page=page, + url=functools.partial(pager_url, state, sort), + item_count=datarequests_list['count'], + items_per_page=limit + ) + c.facet_titles = { + 'state': tk._('State'), + } + + # Organization facet cannot be shown when the user is viewing an org + if include_organization_facet is True: + c.facet_titles['organization'] = tk._('Organizations') + + return tk.render(file_to_render, extra_vars={'user_dict': c.user_dict if hasattr(c, 'user_dict') else None, 'group_type': 'organization'}) + except ValueError as e: + # This exception should only occur if the page value is not valid + log.warn(e) + return tk.abort(400, tk._('"page" parameter must be an integer')) + except tk.NotAuthorized as e: + log.warn(e) + return tk.abort(403, tk._('Unauthorized to list Data Requests')) + + +def index(): + return _show_index(None, request_helpers.get_first_query_param('organization', ''), True, search_url, 'datarequests/index.html') + + +def _process_post(action, context): + # If the user has submitted the form, the data request must be created + if request_helpers.get_post_params(): + data_dict = {} + data_dict['title'] = request_helpers.get_first_post_param('title', '') + data_dict['description'] = request_helpers.get_first_post_param('description', '') + data_dict['organization_id'] = request_helpers.get_first_post_param('organization_id', '') + + if action == constants.UPDATE_DATAREQUEST: + data_dict['id'] = request_helpers.get_first_post_param('id', '') + + try: + result = tk.get_action(action)(context, data_dict) + return tk.redirect_to(tk.url_for('datarequest.show', id=result['id'])) + + except tk.ValidationError as e: + log.warn(e) + # Fill the fields that will display some information in the page + c.datarequest = { + 'id': data_dict.get('id', ''), + 'title': data_dict.get('title', ''), + 'description': data_dict.get('description', ''), + 'organization_id': data_dict.get('organization_id', '') + } + c.errors = e.error_dict + c.errors_summary = _get_errors_summary(c.errors) + + +def new(): + context = _get_context() + + # Basic initialization + c.datarequest = {} + c.errors = {} + c.errors_summary = {} + + # Check access + try: + tk.check_access(constants.CREATE_DATAREQUEST, context, None) + post_result = _process_post(constants.CREATE_DATAREQUEST, context) + return post_result or tk.render('datarequests/new.html') + except tk.NotAuthorized as e: + log.warn(e) + return tk.abort(403, tk._('Unauthorized to create a Data Request')) + + +def show(id): + data_dict = {'id': id} + context = _get_context() + + try: + tk.check_access(constants.SHOW_DATAREQUEST, context, data_dict) + c.datarequest = tk.get_action(constants.SHOW_DATAREQUEST)(context, data_dict) + + context_ignore_auth = context.copy() + context_ignore_auth['ignore_auth'] = True + + return tk.render('datarequests/show.html') + except tk.ObjectNotFound: + return tk.abort(404, tk._('Data Request %s not found') % id) + except tk.NotAuthorized as e: + log.warn(e) + return tk.abort(403, tk._('You are not authorized to view the Data Request %s' % id)) + + +def update(id): + data_dict = {'id': id} + context = _get_context() + + # Basic initialization + c.datarequest = {} + c.errors = {} + c.errors_summary = {} + + try: + tk.check_access(constants.UPDATE_DATAREQUEST, context, data_dict) + c.datarequest = tk.get_action(constants.SHOW_DATAREQUEST)(context, data_dict) + c.original_title = c.datarequest.get('title') + post_result = _process_post(constants.UPDATE_DATAREQUEST, context) + return post_result or tk.render('datarequests/edit.html') + except tk.ObjectNotFound as e: + log.warn(e) + return tk.abort(404, tk._('Data Request %s not found') % id) + except tk.NotAuthorized as e: + log.warn(e) + return tk.abort(403, tk._('You are not authorized to update the Data Request %s' % id)) + + +def delete(id): + data_dict = {'id': id} + context = _get_context() + + try: + tk.check_access(constants.DELETE_DATAREQUEST, context, data_dict) + datarequest = tk.get_action(constants.DELETE_DATAREQUEST)(context, data_dict) + h.flash_notice(tk._('Data Request %s has been deleted') % datarequest.get('title', '')) + return tk.redirect_to(tk.url_for('datarequest.index')) + except tk.ObjectNotFound as e: + log.warn(e) + return tk.abort(404, tk._('Data Request %s not found') % id) + except tk.NotAuthorized as e: + log.warn(e) + return tk.abort(403, tk._('You are not authorized to delete the Data Request %s' % id)) + + +def organization(id): + context = _get_context() + c.group_dict = tk.get_action('organization_show')(context, {'id': id}) + url_func = functools.partial(org_datarequest_url, id=id) + return _show_index(None, id, False, url_func, 'organization/datarequests.html') + + +def user(id): + context = _get_context() + c.user_dict = tk.get_action('user_show')(context, {'id': id, 'include_num_followers': True}) + url_func = functools.partial(user_datarequest_url, id=id) + return _show_index(id, request_helpers.get_first_query_param('organization', ''), True, url_func, 'user/datarequests.html') + + +def close(id): + data_dict = {'id': id} + context = _get_context() + + # Basic initialization + c.datarequest = {} + + def _return_page(errors=None, errors_summary=None): + errors = errors or {} + errors_summary = errors_summary or {} + # Get datasets (if the data req belongs to an organization, + # only the ones that belong to the organization are shown) + # FIXME: At this time, only the 500 last modified/created datasets are retrieved. + # We assume that a user will close their data request with a recently added or modified dataset + # In the future, we should fix this with an autocomplete form... + search_data_dict = {'rows': 500} + organization_id = c.datarequest.get('organization_id', '') + if organization_id: + log.debug("Loading datasets for organisation %s", organization_id) + search_data_dict['q'] = 'owner_org:' + organization_id + else: + # Expected for CKAN 2.3 + log.debug("Loading first 500 datasets...") + base_datasets = tk.get_action('package_search')({'ignore_auth': True}, search_data_dict)['results'] + + log.debug("Dataset candidates for closing data request: %s", base_datasets) + c.datasets = [] + c.errors = errors + c.errors_summary = errors_summary + for dataset in base_datasets: + c.datasets.append({'name': dataset.get('name'), 'title': dataset.get('title')}) + + if h.closing_circumstances_enabled: + # This is required so the form can set the currently selected close_circumstance option in the select dropdown + c.datarequest['close_circumstance'] = request_helpers.get_first_post_param('close_circumstance', None) + + return tk.render('datarequests/close.html') + + try: + tk.check_access(constants.CLOSE_DATAREQUEST, context, data_dict) + c.datarequest = tk.get_action(constants.SHOW_DATAREQUEST)(context, data_dict) + + if c.datarequest.get('closed', False): + return tk.abort(403, tk._('This data request is already closed')) + elif request_helpers.get_post_params(): + data_dict = {} + data_dict['accepted_dataset_id'] = request_helpers.get_first_post_param('accepted_dataset_id', None) + data_dict['id'] = id + if h.closing_circumstances_enabled: + data_dict['close_circumstance'] = request_helpers.get_first_post_param('close_circumstance', None) + data_dict['approx_publishing_date'] = request_helpers.get_first_post_param('approx_publishing_date', None) + data_dict['condition'] = request_helpers.get_first_post_param('condition', None) + + tk.get_action(constants.CLOSE_DATAREQUEST)(context, data_dict) + return tk.redirect_to(tk.url_for('datarequest.show', id=data_dict['id'])) + else: # GET + return _return_page() + + except tk.ValidationError as e: # Accepted Dataset is not valid + log.warn(e) + errors_summary = _get_errors_summary(e.error_dict) + return _return_page(e.error_dict, errors_summary) + except tk.ObjectNotFound as e: + log.warn(e) + return tk.abort(404, tk._('Data Request %s not found') % id) + except tk.NotAuthorized as e: + log.warn(e) + return tk.abort(403, tk._('You are not authorized to close the Data Request %s' % id)) + + +def comment(id): + try: + context = _get_context() + data_dict_comment_list = {'datarequest_id': id} + data_dict_dr_show = {'id': id} + tk.check_access(constants.LIST_DATAREQUEST_COMMENTS, context, data_dict_comment_list) + + # Raises 404 Not Found if the data request does not exist + c.datarequest = tk.get_action(constants.SHOW_DATAREQUEST)(context, data_dict_dr_show) + + comment_text = request_helpers.get_first_post_param('comment', '') + comment_id = request_helpers.get_first_post_param('comment-id', '') + updated_comment = None + + if request_helpers.get_post_params(): + action = constants.COMMENT_DATAREQUEST + action_text = 'comment' + + if comment_id: + action = constants.UPDATE_DATAREQUEST_COMMENT + action_text = 'update comment' + + try: + comment_data_dict = {'datarequest_id': id, 'comment': comment_text, 'id': comment_id} + updated_comment = tk.get_action(action)(context, comment_data_dict) + + if not comment_id: + flash_message = tk._('Comment has been published') + else: + flash_message = tk._('Comment has been updated') + + h.flash_notice(flash_message) + + except tk.NotAuthorized as e: + log.warn(e) + return tk.abort(403, tk._('You are not authorized to %s' % action_text)) + except tk.ValidationError as e: + log.warn(e) + c.errors = e.error_dict + c.errors_summary = _get_errors_summary(c.errors) + except tk.ObjectNotFound as e: + log.warn(e) + return tk.abort(404, tk._(str(e))) + # Other exceptions are not expected. Otherwise, the request will fail. + + # This is required to scroll the user to the appropriate comment + if not updated_comment: + updated_comment = { + 'id': comment_id, + 'comment': comment_text + } + + c.updated_comment = { + 'comment': updated_comment + } + # Comments should be retrieved once that the comment has been created + get_comments_data_dict = {'datarequest_id': id} + c.comments = tk.get_action(constants.LIST_DATAREQUEST_COMMENTS)(context, get_comments_data_dict) + + return tk.render('datarequests/comment.html') + + except tk.ObjectNotFound as e: + log.warn(e) + return tk.abort(404, tk._('Data Request %s not found' % id)) + + except tk.NotAuthorized as e: + log.warn(e) + return tk.abort(403, tk._('You are not authorized to list the comments of the Data Request %s' % id)) + + +def delete_comment(datarequest_id, comment_id): + try: + context = _get_context() + data_dict = {'id': comment_id} + tk.check_access(constants.DELETE_DATAREQUEST_COMMENT, context, data_dict) + tk.get_action(constants.DELETE_DATAREQUEST_COMMENT)(context, data_dict) + h.flash_notice(tk._('Comment has been deleted')) + return tk.redirect_to(tk.url_for('datarequest.comment', id=datarequest_id)) + except tk.ObjectNotFound as e: + log.warn(e) + return tk.abort(404, tk._('Comment %s not found') % comment_id) + except tk.NotAuthorized as e: + log.warn(e) + return tk.abort(403, tk._('You are not authorized to delete this comment')) + + +def follow(datarequest_id): + # Method is not called + pass + + +def unfollow(datarequest_id): + # Method is not called + pass diff --git a/ckanext/datarequests/controllers/ui_controller.py b/ckanext/datarequests/controllers/ui_controller.py index 71419256..7555e639 100644 --- a/ckanext/datarequests/controllers/ui_controller.py +++ b/ckanext/datarequests/controllers/ui_controller.py @@ -1,428 +1,44 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015-2016 CoNWeT Lab., Universidad Politécnica de Madrid +from ckan.plugins.toolkit import BaseController -# This file is part of CKAN Data Requests Extension. +from . import controller_functions as controller -# CKAN Data Requests Extension 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. -# CKAN Data Requests Extension 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 CKAN Data Requests Extension. If not, see . - -import logging - -import ckan.lib.base as base -import ckan.model as model -import ckan.plugins as plugins -import ckan.lib.helpers as helpers -import ckanext.datarequests.constants as constants -import functools -import re - -from ckan.common import request -from urllib import urlencode - - -_link = re.compile(r'(?:(https?://)|(www\.))(\S+\b/?)([!"#$%&\'()*+,\-./:;<=>?@[\\\]^_`{|}~]*)(\s|$)', re.I) - -log = logging.getLogger(__name__) -tk = plugins.toolkit -c = tk.c - - -def _get_errors_summary(errors): - errors_summary = {} - - for key, error in errors.items(): - errors_summary[key] = ', '.join(error) - - return errors_summary - - -def _encode_params(params): - return [(k, v.encode('utf-8') if isinstance(v, basestring) else str(v)) - for k, v in params] - - -def url_with_params(url, params): - params = _encode_params(params) - return url + u'?' + urlencode(params) - - -def search_url(params): - url = helpers.url_for(controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='index') - return url_with_params(url, params) - - -def org_datarequest_url(params, id): - url = helpers.url_for(controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='organization_datarequests', id=id) - return url_with_params(url, params) - - -def user_datarequest_url(params, id): - url = helpers.url_for(controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='user_datarequests', id=id) - return url_with_params(url, params) - - -class DataRequestsUI(base.BaseController): - - def _get_context(self): - return {'model': model, 'session': model.Session, - 'user': c.user, 'auth_user_obj': c.userobj} - - def _show_index(self, user_id, organization_id, include_organization_facet, url_func, file_to_render): - - def pager_url(state=None, sort=None, q=None, page=None): - params = list() - - if q: - params.append(('q', q)) - - if state is not None: - params.append(('state', state)) - - params.append(('sort', sort)) - params.append(('page', page)) - - return url_func(params) - - try: - context = self._get_context() - page = int(request.GET.get('page', 1)) - limit = constants.DATAREQUESTS_PER_PAGE - offset = (page - 1) * constants.DATAREQUESTS_PER_PAGE - data_dict = {'offset': offset, 'limit': limit} - - state = request.GET.get('state', None) - if state: - data_dict['closed'] = True if state == 'closed' else False - - q = request.GET.get('q', '') - if q: - data_dict['q'] = q - - if organization_id: - data_dict['organization_id'] = organization_id - - if user_id: - data_dict['user_id'] = user_id - - sort = request.GET.get('sort', 'desc') - sort = sort if sort in ['asc', 'desc'] else 'desc' - if sort is not None: - data_dict['sort'] = sort - - tk.check_access(constants.LIST_DATAREQUESTS, context, data_dict) - datarequests_list = tk.get_action(constants.LIST_DATAREQUESTS)(context, data_dict) - - c.filters = [(tk._('Newest'), 'desc'), (tk._('Oldest'), 'asc')] - c.sort = sort - c.q = q - c.organization = organization_id - c.state = state - c.datarequest_count = datarequests_list['count'] - c.datarequests = datarequests_list['result'] - c.search_facets = datarequests_list['facets'] - c.page = helpers.Page( - collection=datarequests_list['result'], - page=page, - url=functools.partial(pager_url, state, sort), - item_count=datarequests_list['count'], - items_per_page=limit - ) - c.facet_titles = { - 'state': tk._('State'), - } - - # Organization facet cannot be shown when the user is viewing an org - if include_organization_facet is True: - c.facet_titles['organization'] = tk._('Organizations') - - return tk.render(file_to_render, extra_vars={'user_dict': c.user_dict if hasattr(c, 'user_dict') else None, 'group_type': 'organization'}) - except ValueError as e: - # This exception should only occur if the page value is not valid - log.warn(e) - tk.abort(400, tk._('"page" parameter must be an integer')) - except tk.NotAuthorized as e: - log.warn(e) - tk.abort(403, tk._('Unauthorized to list Data Requests')) +class DataRequestsUI(BaseController): def index(self): - return self._show_index(None, request.GET.get('organization', ''), True, search_url, 'datarequests/index.html') - - def _process_post(self, action, context): - # If the user has submitted the form, the data request must be created - if request.POST: - data_dict = {} - data_dict['title'] = request.POST.get('title', '') - data_dict['description'] = request.POST.get('description', '') - data_dict['organization_id'] = request.POST.get('organization_id', '') - - if action == constants.UPDATE_DATAREQUEST: - data_dict['id'] = request.POST.get('id', '') - - try: - result = tk.get_action(action)(context, data_dict) - tk.redirect_to(helpers.url_for(controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='show', id=result['id'])) - - except tk.ValidationError as e: - log.warn(e) - # Fill the fields that will display some information in the page - c.datarequest = { - 'id': data_dict.get('id', ''), - 'title': data_dict.get('title', ''), - 'description': data_dict.get('description', ''), - 'organization_id': data_dict.get('organization_id', '') - } - c.errors = e.error_dict - c.errors_summary = _get_errors_summary(c.errors) + return controller.index() def new(self): - context = self._get_context() - - # Basic intialization - c.datarequest = {} - c.errors = {} - c.errors_summary = {} - - # Check access - try: - tk.check_access(constants.CREATE_DATAREQUEST, context, None) - self._process_post(constants.CREATE_DATAREQUEST, context) - - # The form is always rendered - return tk.render('datarequests/new.html') - - except tk.NotAuthorized as e: - log.warn(e) - tk.abort(403, tk._('Unauthorized to create a Data Request')) + return controller.new() def show(self, id): - data_dict = {'id': id} - context = self._get_context() - - try: - tk.check_access(constants.SHOW_DATAREQUEST, context, data_dict) - c.datarequest = tk.get_action(constants.SHOW_DATAREQUEST)(context, data_dict) - - context_ignore_auth = context.copy() - context_ignore_auth['ignore_auth'] = True - - return tk.render('datarequests/show.html') - except tk.ObjectNotFound as e: - tk.abort(404, tk._('Data Request %s not found') % id) - except tk.NotAuthorized as e: - log.warn(e) - tk.abort(403, tk._('You are not authorized to view the Data Request %s' - % id)) + return controller.show(id) def update(self, id): - data_dict = {'id': id} - context = self._get_context() - - # Basic intialization - c.datarequest = {} - c.errors = {} - c.errors_summary = {} - - try: - tk.check_access(constants.UPDATE_DATAREQUEST, context, data_dict) - c.datarequest = tk.get_action(constants.SHOW_DATAREQUEST)(context, data_dict) - c.original_title = c.datarequest.get('title') - self._process_post(constants.UPDATE_DATAREQUEST, context) - return tk.render('datarequests/edit.html') - except tk.ObjectNotFound as e: - log.warn(e) - tk.abort(404, tk._('Data Request %s not found') % id) - except tk.NotAuthorized as e: - log.warn(e) - tk.abort(403, tk._('You are not authorized to update the Data Request %s' - % id)) + return controller.update(id) def delete(self, id): - data_dict = {'id': id} - context = self._get_context() - - try: - tk.check_access(constants.DELETE_DATAREQUEST, context, data_dict) - datarequest = tk.get_action(constants.DELETE_DATAREQUEST)(context, data_dict) - helpers.flash_notice(tk._('Data Request %s has been deleted') % datarequest.get('title', '')) - tk.redirect_to(helpers.url_for(controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='index')) - except tk.ObjectNotFound as e: - log.warn(e) - tk.abort(404, tk._('Data Request %s not found') % id) - except tk.NotAuthorized as e: - log.warn(e) - tk.abort(403, tk._('You are not authorized to delete the Data Request %s' - % id)) + return controller.delete(id) def organization_datarequests(self, id): - context = self._get_context() - c.group_dict = tk.get_action('organization_show')(context, {'id': id}) - url_func = functools.partial(org_datarequest_url, id=id) - return self._show_index(None, id, False, url_func, 'organization/datarequests.html') + return controller.organization(id) def user_datarequests(self, id): - context = self._get_context() - c.user_dict = tk.get_action('user_show')(context, {'id': id, 'include_num_followers': True}) - url_func = functools.partial(user_datarequest_url, id=id) - return self._show_index(id, request.GET.get('organization', ''), True, url_func, 'user/datarequests.html') + return controller.user(id) def close(self, id): - data_dict = {'id': id} - context = self._get_context() - - # Basic intialization - c.datarequest = {} - - def _return_page(errors={}, errors_summary={}): - # Get datasets (if the data req belongs to an organization, only the one that - # belongs to the organization are shown) - organization_id = c.datarequest.get('organization_id', '') - if organization_id: - base_datasets = tk.get_action('organization_show')({'ignore_auth': True}, {'id': organization_id, 'include_datasets': True})['packages'] - else: - # FIXME: At this time, only the 500 last modified/created datasets are retrieved. - # We assume that a user will close their data request with a recently added or modified dataset - # In the future, we should fix this with an autocomplete form... - # Expected for CKAN 2.3 - base_datasets = tk.get_action('package_search')({'ignore_auth': True}, {'rows': 500})['results'] - - c.datasets = [] - c.errors = errors - c.errors_summary = errors_summary - for dataset in base_datasets: - c.datasets.append({'name': dataset.get('name'), 'title': dataset.get('title')}) - - return tk.render('datarequests/close.html') - - try: - tk.check_access(constants.CLOSE_DATAREQUEST, context, data_dict) - c.datarequest = tk.get_action(constants.SHOW_DATAREQUEST)(context, data_dict) - - if c.datarequest.get('closed', False): - tk.abort(403, tk._('This data request is already closed')) - elif request.POST: - data_dict = {} - data_dict['accepted_dataset_id'] = request.POST.get('accepted_dataset_id', None) - data_dict['id'] = id - - tk.get_action(constants.CLOSE_DATAREQUEST)(context, data_dict) - tk.redirect_to(helpers.url_for(controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='show', id=data_dict['id'])) - else: # GET - return _return_page() - - except tk.ValidationError as e: # Accepted Dataset is not valid - log.warn(e) - errors_summary = _get_errors_summary(e.error_dict) - return _return_page(e.error_dict, errors_summary) - except tk.ObjectNotFound as e: - log.warn(e) - tk.abort(404, tk._('Data Request %s not found') % id) - except tk.NotAuthorized as e: - log.warn(e) - tk.abort(403, tk._('You are not authorized to close the Data Request %s' - % id)) + return controller.close(id) def comment(self, id): - try: - context = self._get_context() - data_dict_comment_list = {'datarequest_id': id} - data_dict_dr_show = {'id': id} - tk.check_access(constants.LIST_DATAREQUEST_COMMENTS, context, data_dict_comment_list) - - # Raises 404 Not Found if the data request does not exist - c.datarequest = tk.get_action(constants.SHOW_DATAREQUEST)(context, data_dict_dr_show) - - comment_text = request.POST.get('comment', '') - comment_id = request.POST.get('comment-id', '') - - if request.POST: - action = constants.COMMENT_DATAREQUEST - action_text = 'comment' - - if comment_id: - action = constants.UPDATE_DATAREQUEST_COMMENT - action_text = 'update comment' - - try: - comment_data_dict = {'datarequest_id': id, 'comment': comment_text, 'id': comment_id} - updated_comment = tk.get_action(action)(context, comment_data_dict) - - if not comment_id: - flash_message = tk._('Comment has been published') - else: - flash_message = tk._('Comment has been updated') - - helpers.flash_notice(flash_message) - - except tk.NotAuthorized as e: - log.warn(e) - tk.abort(403, tk._('You are not authorized to %s' % action_text)) - except tk.ValidationError as e: - log.warn(e) - c.errors = e.error_dict - c.errors_summary = _get_errors_summary(c.errors) - except tk.ObjectNotFound as e: - log.warn(e) - tk.abort(404, tk._(str(e))) - # Other exceptions are not expected. Otherwise, the request will fail. - - # This is required to scroll the user to the appropriate comment - if 'updated_comment' in locals(): - c.updated_comment = updated_comment - else: - c.updated_comment = { - 'id': comment_id, - 'comment': comment_text - } - - # Comments should be retrieved once that the comment has been created - get_comments_data_dict = {'datarequest_id': id} - c.comments = tk.get_action(constants.LIST_DATAREQUEST_COMMENTS)(context, get_comments_data_dict) - - return tk.render('datarequests/comment.html') - - except tk.ObjectNotFound as e: - log.warn(e) - tk.abort(404, tk._('Data Request %s not found' % id)) - - except tk.NotAuthorized as e: - log.warn(e) - tk.abort(403, tk._('You are not authorized to list the comments of the Data Request %s' - % id)) + return controller.comment(id) def delete_comment(self, datarequest_id, comment_id): - try: - context = self._get_context() - data_dict = {'id': comment_id} - tk.check_access(constants.DELETE_DATAREQUEST_COMMENT, context, data_dict) - tk.get_action(constants.DELETE_DATAREQUEST_COMMENT)(context, data_dict) - helpers.flash_notice(tk._('Comment has been deleted')) - tk.redirect_to(helpers.url_for(controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='comment', id=datarequest_id)) - except tk.ObjectNotFound as e: - log.warn(e) - tk.abort(404, tk._('Comment %s not found') % comment_id) - except tk.NotAuthorized as e: - log.warn(e) - tk.abort(403, tk._('You are not authorized to delete this comment')) + return controller.delete_comment(datarequest_id, comment_id) def follow(self, datarequest_id): - # Method is not called - pass + return controller.follow(datarequest_id) def unfollow(self, datarequest_id): - # Method is not called - pass - + return controller.unfollow(datarequest_id) diff --git a/ckanext/datarequests/db.py b/ckanext/datarequests/db.py index 1f36ac91..94538466 100644 --- a/ckanext/datarequests/db.py +++ b/ckanext/datarequests/db.py @@ -17,13 +17,16 @@ # You should have received a copy of the GNU Affero General Public License # along with CKAN Data Requests Extension. If not, see . -import constants import sqlalchemy as sa import uuid +import logging +import ckan.plugins.toolkit as tk +from ckanext.datarequests import constants -from sqlalchemy import func +from sqlalchemy import func, MetaData, DDL from sqlalchemy.sql.expression import or_ +log = logging.getLogger(__name__) DataRequest = None Comment = None DataRequestFollower = None @@ -38,6 +41,8 @@ def init_db(model): global DataRequest global Comment global DataRequestFollower + global closing_circumstances_enabled + closing_circumstances_enabled = tk.h.closing_circumstances_enabled if DataRequest is None: @@ -88,22 +93,27 @@ def get_open_datarequests_number(cls): # FIXME: References to the other tables... datarequests_table = sa.Table('datarequests', model.meta.metadata, - sa.Column('user_id', sa.types.UnicodeText, primary_key=False, default=u''), - sa.Column('id', sa.types.UnicodeText, primary_key=True, default=uuid4), - sa.Column('title', sa.types.Unicode(constants.NAME_MAX_LENGTH), primary_key=True, default=u''), - sa.Column('description', sa.types.Unicode(constants.DESCRIPTION_MAX_LENGTH), primary_key=False, default=u''), - sa.Column('organization_id', sa.types.UnicodeText, primary_key=False, default=None), - sa.Column('open_time', sa.types.DateTime, primary_key=False, default=None), - sa.Column('accepted_dataset_id', sa.types.UnicodeText, primary_key=False, default=None), - sa.Column('close_time', sa.types.DateTime, primary_key=False, default=None), - sa.Column('closed', sa.types.Boolean, primary_key=False, default=False) - ) + sa.Column('user_id', sa.types.UnicodeText, primary_key=False, default=u''), + sa.Column('id', sa.types.UnicodeText, primary_key=True, default=uuid4), + sa.Column('title', sa.types.Unicode(constants.NAME_MAX_LENGTH), primary_key=True, default=u''), + sa.Column('description', sa.types.Unicode(constants.DESCRIPTION_MAX_LENGTH), primary_key=False, default=u''), + sa.Column('organization_id', sa.types.UnicodeText, primary_key=False, default=None), + sa.Column('open_time', sa.types.DateTime, primary_key=False, default=None), + sa.Column('accepted_dataset_id', sa.types.UnicodeText, primary_key=False, default=None), + sa.Column('close_time', sa.types.DateTime, primary_key=False, default=None), + sa.Column('closed', sa.types.Boolean, primary_key=False, default=False), + sa.Column('close_circumstance', sa.types.Unicode(constants.CLOSE_CIRCUMSTANCE_MAX_LENGTH), primary_key=False, default=u'') + if closing_circumstances_enabled else None, + sa.Column('approx_publishing_date', sa.types.DateTime, primary_key=False, default=None) + if closing_circumstances_enabled else None + ) # Create the table only if it does not exist datarequests_table.create(checkfirst=True) - model.meta.mapper(DataRequest, datarequests_table,) + model.meta.mapper(DataRequest, datarequests_table) + update_db(model) if Comment is None: class _Comment(model.DomainObject): @@ -132,12 +142,12 @@ def get_comment_datarequests_number(cls, **kw): # FIXME: References to the other tables... comments_table = sa.Table('datarequests_comments', model.meta.metadata, - sa.Column('id', sa.types.UnicodeText, primary_key=True, default=uuid4), - sa.Column('user_id', sa.types.UnicodeText, primary_key=False, default=u''), - sa.Column('datarequest_id', sa.types.UnicodeText, primary_key=True, default=uuid4), - sa.Column('time', sa.types.DateTime, primary_key=True, default=u''), - sa.Column('comment', sa.types.Unicode(constants.COMMENT_MAX_LENGTH), primary_key=False, default=u'') - ) + sa.Column('id', sa.types.UnicodeText, primary_key=True, default=uuid4), + sa.Column('user_id', sa.types.UnicodeText, primary_key=False, default=u''), + sa.Column('datarequest_id', sa.types.UnicodeText, primary_key=True, default=uuid4), + sa.Column('time', sa.types.DateTime, primary_key=True, default=u''), + sa.Column('comment', sa.types.Unicode(constants.COMMENT_MAX_LENGTH), primary_key=False, default=u'') + ) # Create the table only if it does not exist comments_table.create(checkfirst=True) @@ -164,14 +174,32 @@ def get_datarequest_followers_number(cls, **kw): # FIXME: References to the other tables... followers_table = sa.Table('datarequests_followers', model.meta.metadata, - sa.Column('id', sa.types.UnicodeText, primary_key=True, default=uuid4), - sa.Column('user_id', sa.types.UnicodeText, primary_key=False, default=u''), - sa.Column('datarequest_id', sa.types.UnicodeText, primary_key=True, default=uuid4), - sa.Column('time', sa.types.DateTime, primary_key=True, default=u'') - ) + sa.Column('id', sa.types.UnicodeText, primary_key=True, default=uuid4), + sa.Column('user_id', sa.types.UnicodeText, primary_key=False, default=u''), + sa.Column('datarequest_id', sa.types.UnicodeText, primary_key=True, default=uuid4), + sa.Column('time', sa.types.DateTime, primary_key=True, default=u'') + ) # Create the table only if it does not exist followers_table.create(checkfirst=True) model.meta.mapper(DataRequestFollower, followers_table,) + +def update_db(model): + ''' + A place to make any datarequest table updates via SQL commands + This is required because adding new columns to sqlalchemy metadata will not get created if the table already exists + ''' + meta = MetaData(bind=model.Session.get_bind(), reflect=True) + + # Check to see if columns exists and create them if they do not exists + if closing_circumstances_enabled: + if 'datarequests' in meta.tables: + if 'close_circumstance' not in meta.tables['datarequests'].columns: + log.info("DataRequests-UpdateDB: 'close_circumstance' field does not exist, adding...") + DDL('ALTER TABLE "datarequests" ADD COLUMN "close_circumstance" varchar({0}) NULL'.format(constants.CLOSE_CIRCUMSTANCE_MAX_LENGTH)).execute(model.Session.get_bind()) + + if 'approx_publishing_date' not in meta.tables['datarequests'].columns: + log.info("DataRequests-UpdateDB: 'approx_publishing_date' field does not exist, adding...") + DDL('ALTER TABLE "datarequests" ADD COLUMN "approx_publishing_date" timestamp NULL').execute(model.Session.get_bind()) diff --git a/ckanext/datarequests/fanstatic/datarequest_close.js b/ckanext/datarequests/fanstatic/datarequest_close.js new file mode 100644 index 00000000..01f631b1 --- /dev/null +++ b/ckanext/datarequests/fanstatic/datarequest_close.js @@ -0,0 +1,36 @@ + +jQuery(document).ready(function () { + + // On page load select the first circumstance option + showCondition(jQuery('#field-close_circumstance').find(':selected').data('condition')) + + // On change check if the selected circumstance has a condition + jQuery('#field-close_circumstance').change(function () { + showCondition(jQuery('#field-close_circumstance').find(':selected').data('condition')); + }); + + // Show the elements for the selected condition + function showCondition(condition) { + jQuery('#field-condition').val(condition) + clearConditionElements() + + if (condition === 'nominate_dataset') { + jQuery('#field-accepted_dataset_id').parent().parent().show() + jQuery('#field-approx_publishing_date').parent().parent().hide() + } + else if (condition === 'nominate_approximate_date') { + jQuery('#field-accepted_dataset_id').parent().parent().hide() + jQuery('#field-approx_publishing_date').parent().parent().show() + } + else { + jQuery('#field-accepted_dataset_id').parent().parent().hide() + jQuery('#field-approx_publishing_date').parent().parent().hide() + } + } + + function clearConditionElements() { + jQuery('#field-accepted_dataset_id').val('') + jQuery('#field-approx_publishing_date').val('') + } + +}); \ No newline at end of file diff --git a/ckanext/datarequests/helpers.py b/ckanext/datarequests/helpers.py index d6c7787f..875341b6 100644 --- a/ckanext/datarequests/helpers.py +++ b/ckanext/datarequests/helpers.py @@ -17,11 +17,11 @@ # You should have received a copy of the GNU Affero General Public License # along with CKAN Data Requests Extension. If not, see . -import ckan.model as model +from ckan import model +from ckan.common import c import ckan.plugins.toolkit as tk -import db -from ckan.common import c +from . import db def get_comments_number(datarequest_id): @@ -54,3 +54,19 @@ def get_open_datarequests_badge(show_badge): {'comments_count': get_open_datarequests_number()}) else: return '' + + +def get_closing_circumstances(): + """Returns a list of datarequest closing circumstances from admin config + + :rtype: List of circumstance objects {'circumstance': circumstance, 'condition': condition} + + """ + closing_circumstances = [] + for closing_circumstance in tk.config.get('ckan.datarequests.closing_circumstances', '').split('\n'): + option = closing_circumstance.split('|') + circumstance = option[0].strip() + condition = option[1].strip() if len(option) == 2 else '' + closing_circumstances.append({'circumstance': circumstance, 'condition': condition}) + + return closing_circumstances diff --git a/ckanext/datarequests/i18n/ckanext-datarequests.pot b/ckanext/datarequests/i18n/ckanext-datarequests.pot index 290de903..cf20e5ba 100644 --- a/ckanext/datarequests/i18n/ckanext-datarequests.pot +++ b/ckanext/datarequests/i18n/ckanext-datarequests.pot @@ -1,53 +1,63 @@ # Translations template for ckanext-datarequests. -# Copyright (C) 2016 ORGANIZATION +# Copyright (C) 2019 ORGANIZATION # This file is distributed under the same license as the ckanext-datarequests # project. -# FIRST AUTHOR , 2016. +# FIRST AUTHOR , 2019. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: ckanext-datarequests 0.3.1\n" +"Project-Id-Version: ckanext-datarequests 1.1.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2016-04-28 14:45+0200\n" +"POT-Creation-Date: 2019-12-14 14:25+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 0.9.6\n" +"Generated-By: Babel 2.3.4\n" -#: ckanext/datarequests/actions.py:192 ckanext/datarequests/actions.py:244 -#: ckanext/datarequests/actions.py:435 ckanext/datarequests/actions.py:480 -#: ckanext/datarequests/actions.py:536 ckanext/datarequests/actions.py:624 +#: ckanext/datarequests/actions.py:251 ckanext/datarequests/actions.py:304 +#: ckanext/datarequests/actions.py:496 ckanext/datarequests/actions.py:542 +#: ckanext/datarequests/actions.py:604 ckanext/datarequests/actions.py:696 +#: ckanext/datarequests/actions.py:830 ckanext/datarequests/actions.py:881 msgid "Data Request ID has not been included" msgstr "" -#: ckanext/datarequests/actions.py:203 ckanext/datarequests/actions.py:255 -#: ckanext/datarequests/actions.py:446 ckanext/datarequests/actions.py:491 +#: ckanext/datarequests/actions.py:262 ckanext/datarequests/actions.py:315 +#: ckanext/datarequests/actions.py:507 ckanext/datarequests/actions.py:553 +#: ckanext/datarequests/actions.py:841 #, python-format msgid "Data Request %s not found in the data base" msgstr "" -#: ckanext/datarequests/actions.py:500 +#: ckanext/datarequests/actions.py:562 msgid "This Data Request is already closed" msgstr "" -#: ckanext/datarequests/actions.py:578 ckanext/datarequests/actions.py:671 -#: ckanext/datarequests/actions.py:717 +#: ckanext/datarequests/actions.py:650 ckanext/datarequests/actions.py:743 +#: ckanext/datarequests/actions.py:789 msgid "Comment ID has not been included" msgstr "" -#: ckanext/datarequests/actions.py:589 ckanext/datarequests/actions.py:682 -#: ckanext/datarequests/actions.py:728 +#: ckanext/datarequests/actions.py:661 ckanext/datarequests/actions.py:754 +#: ckanext/datarequests/actions.py:800 #, python-format msgid "Comment %s not found in the data base" msgstr "" +#: ckanext/datarequests/actions.py:847 +msgid "The user is already following the given Data Request" +msgstr "" + +#: ckanext/datarequests/actions.py:893 +msgid "The user is not following the given Data Request" +msgstr "" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:18 #: ckanext/datarequests/validator.py:31 ckanext/datarequests/validator.py:34 #: ckanext/datarequests/validator.py:41 -#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:17 msgid "Title" msgstr "" @@ -64,8 +74,8 @@ msgstr "" msgid "That title is already in use" msgstr "" +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:22 #: ckanext/datarequests/validator.py:45 -#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:21 msgid "Description" msgstr "" @@ -74,9 +84,9 @@ msgstr "" msgid "Description must be a maximum of %d characters long" msgstr "" -#: ckanext/datarequests/validator.py:52 #: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:11 -#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:25 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:26 +#: ckanext/datarequests/validator.py:52 msgid "Organization" msgstr "" @@ -84,8 +94,8 @@ msgstr "" msgid "Organization is not valid" msgstr "" -#: ckanext/datarequests/validator.py:65 #: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:24 +#: ckanext/datarequests/validator.py:65 msgid "Accepted Dataset" msgstr "" @@ -93,9 +103,9 @@ msgstr "" msgid "Dataset not found" msgstr "" -#: ckanext/datarequests/validator.py:75 #: ckanext/datarequests/templates/datarequests/base.html:17 #: ckanext/datarequests/templates/datarequests/show.html:25 +#: ckanext/datarequests/validator.py:75 msgid "Data Request" msgstr "" @@ -143,83 +153,83 @@ msgstr "" msgid "Unauthorized to list Data Requests" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:210 +#: ckanext/datarequests/controllers/ui_controller.py:209 msgid "Unauthorized to create a Data Request" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:225 -#: ckanext/datarequests/controllers/ui_controller.py:248 -#: ckanext/datarequests/controllers/ui_controller.py:265 -#: ckanext/datarequests/controllers/ui_controller.py:334 -#: ckanext/datarequests/controllers/ui_controller.py:401 +#: ckanext/datarequests/controllers/ui_controller.py:224 +#: ckanext/datarequests/controllers/ui_controller.py:247 +#: ckanext/datarequests/controllers/ui_controller.py:264 +#: ckanext/datarequests/controllers/ui_controller.py:332 +#: ckanext/datarequests/controllers/ui_controller.py:399 #, python-format msgid "Data Request %s not found" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:228 +#: ckanext/datarequests/controllers/ui_controller.py:227 #, python-format msgid "You are not authorized to view the Data Request %s" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:251 +#: ckanext/datarequests/controllers/ui_controller.py:250 #, python-format msgid "You are not authorized to update the Data Request %s" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:261 -#: ckanext/datarequests/tests/test_ui_controller.py:672 +#: ckanext/datarequests/controllers/ui_controller.py:260 +#: ckanext/datarequests/tests/test_ui_controller.py:673 #, python-format msgid "Data Request %s has been deleted" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:268 +#: ckanext/datarequests/controllers/ui_controller.py:267 #, python-format msgid "You are not authorized to delete the Data Request %s" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:316 +#: ckanext/datarequests/controllers/ui_controller.py:315 msgid "This data request is already closed" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:337 +#: ckanext/datarequests/controllers/ui_controller.py:335 #, python-format msgid "You are not authorized to close the Data Request %s" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:366 +#: ckanext/datarequests/controllers/ui_controller.py:364 msgid "Comment has been published" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:368 +#: ckanext/datarequests/controllers/ui_controller.py:366 msgid "Comment has been updated" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:374 +#: ckanext/datarequests/controllers/ui_controller.py:372 #, python-format msgid "You are not authorized to %s" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:405 +#: ckanext/datarequests/controllers/ui_controller.py:403 #, python-format msgid "You are not authorized to list the comments of the Data Request %s" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:414 +#: ckanext/datarequests/controllers/ui_controller.py:412 msgid "Comment has been deleted" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:419 +#: ckanext/datarequests/controllers/ui_controller.py:416 #, python-format msgid "Comment %s not found" msgstr "" -#: ckanext/datarequests/controllers/ui_controller.py:422 +#: ckanext/datarequests/controllers/ui_controller.py:419 msgid "You are not authorized to delete this comment" msgstr "" #: ckanext/datarequests/templates/header.html:5 #: ckanext/datarequests/templates/organization/read_base.html:4 -#: ckanext/datarequests/templates/user/read_base.html:4 +#: ckanext/datarequests/templates/user/read_base.html:7 msgid "Datasets" msgstr "" @@ -227,15 +237,15 @@ msgstr "" msgid "Groups" msgstr "" -#: ckanext/datarequests/templates/header.html:8 #: ckanext/datarequests/templates/datarequests/base.html:8 #: ckanext/datarequests/templates/datarequests/base.html:11 #: ckanext/datarequests/templates/datarequests/close.html:6 #: ckanext/datarequests/templates/datarequests/edit.html:6 #: ckanext/datarequests/templates/datarequests/new.html:6 #: ckanext/datarequests/templates/datarequests/show.html:8 +#: ckanext/datarequests/templates/header.html:8 #: ckanext/datarequests/templates/organization/read_base.html:6 -#: ckanext/datarequests/templates/user/read_base.html:6 +#: ckanext/datarequests/templates/user/read_base.html:9 msgid "Data Requests" msgstr "" @@ -255,7 +265,7 @@ msgstr "" #: ckanext/datarequests/templates/datarequests/close.html:3 #: ckanext/datarequests/templates/datarequests/close.html:8 #: ckanext/datarequests/templates/datarequests/close.html:12 -#: ckanext/datarequests/templates/datarequests/snippets/close_datarequest_form.html:25 +#: ckanext/datarequests/templates/datarequests/snippets/close_datarequest_form.html:26 msgid "Close Data Request" msgstr "" @@ -284,7 +294,7 @@ msgstr "" #: ckanext/datarequests/templates/datarequests/new.html:3 #: ckanext/datarequests/templates/datarequests/new.html:7 #: ckanext/datarequests/templates/datarequests/new.html:11 -#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:47 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:48 #: ckanext/datarequests/templates/datarequests/snippets/new_datarequest_form.html:7 msgid "Create Data Request" msgstr "" @@ -338,7 +348,7 @@ msgstr "" msgid "Not closed yet" msgstr "" -#: ckanext/datarequests/templates/datarequests/snippets/close_datarequest_form.html:11 +#: ckanext/datarequests/templates/datarequests/snippets/close_datarequest_form.html:12 msgid "Accep. Dataset" msgstr "" @@ -383,23 +393,23 @@ msgstr "" msgid "This data request has not been commented yet" msgstr "" -#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:17 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:18 msgid "eg. Data Request Name" msgstr "" -#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:21 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:22 msgid "eg. Data Request description" msgstr "" -#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:29 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:30 msgid "No organization" msgstr "" -#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:43 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:44 msgid "Are you sure you want to delete this data request?" msgstr "" -#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:44 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:45 msgid "Delete" msgstr "" @@ -419,52 +429,20 @@ msgstr "" msgid "Update Data Request" msgstr "" -#: ckanext/datarequests/templates/home/snippets/stats.html:5 -msgid "{0} statistics" -msgstr "" - -#: ckanext/datarequests/templates/home/snippets/stats.html:10 -msgid "dataset" -msgstr "" - -#: ckanext/datarequests/templates/home/snippets/stats.html:10 -msgid "datasets" -msgstr "" - -#: ckanext/datarequests/templates/home/snippets/stats.html:16 -msgid "organization" -msgstr "" - -#: ckanext/datarequests/templates/home/snippets/stats.html:16 -msgid "organizations" -msgstr "" - -#: ckanext/datarequests/templates/home/snippets/stats.html:22 -msgid "group" -msgstr "" - -#: ckanext/datarequests/templates/home/snippets/stats.html:22 -msgid "groups" -msgstr "" - -#: ckanext/datarequests/templates/home/snippets/stats.html:28 -msgid "related item" -msgstr "" - -#: ckanext/datarequests/templates/home/snippets/stats.html:28 -msgid "related items" +#: ckanext/datarequests/templates/datarequests/snippets/followers.html:11 +msgid "Followers" msgstr "" -#: ckanext/datarequests/templates/home/snippets/stats.html:34 -msgid "data request" +#: ckanext/datarequests/templates/datarequests/snippets/followers.html:22 +msgid "Unfollow" msgstr "" -#: ckanext/datarequests/templates/home/snippets/stats.html:34 -msgid "data requests" +#: ckanext/datarequests/templates/datarequests/snippets/followers.html:27 +msgid "Follow" msgstr "" #: ckanext/datarequests/templates/organization/read_base.html:5 -#: ckanext/datarequests/templates/user/read_base.html:5 +#: ckanext/datarequests/templates/user/read_base.html:8 msgid "Activity Stream" msgstr "" diff --git a/ckanext/datarequests/i18n/fr/LC_MESSAGES/ckanext-datarequests.mo b/ckanext/datarequests/i18n/fr/LC_MESSAGES/ckanext-datarequests.mo new file mode 100644 index 00000000..db5bf07d Binary files /dev/null and b/ckanext/datarequests/i18n/fr/LC_MESSAGES/ckanext-datarequests.mo differ diff --git a/ckanext/datarequests/i18n/fr/LC_MESSAGES/ckanext-datarequests.po b/ckanext/datarequests/i18n/fr/LC_MESSAGES/ckanext-datarequests.po new file mode 100644 index 00000000..70474312 --- /dev/null +++ b/ckanext/datarequests/i18n/fr/LC_MESSAGES/ckanext-datarequests.po @@ -0,0 +1,505 @@ +# French translations for ckanext-datarequests. +# Copyright (C) 2018 SICTIAM +# This file is distributed under the same license as the +# ckanext-datarequests project. +# Benoit Orihuela , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: ckanext-datarequests 0.3.1\n" +"Report-Msgid-Bugs-To: data@metropolegrenoble.fr\n" +"POT-Creation-Date: 2016-04-28 14:45+0200\n" +"PO-Revision-Date: 2017-01-13 10:02+0100\n" +"Last-Translator: Benoit Orihuela \n" +"Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.3.4\n" + +#: ckanext/datarequests/actions.py:192 ckanext/datarequests/actions.py:244 +#: ckanext/datarequests/actions.py:435 ckanext/datarequests/actions.py:480 +#: ckanext/datarequests/actions.py:536 ckanext/datarequests/actions.py:624 +msgid "Data Request ID has not been included" +msgstr "L'ID de la demande n'a pas été ajouté" + +#: ckanext/datarequests/actions.py:203 ckanext/datarequests/actions.py:255 +#: ckanext/datarequests/actions.py:446 ckanext/datarequests/actions.py:491 +#, python-format +msgid "Data Request %s not found in the data base" +msgstr "La demande %s n'a pas été trouvée" + +#: ckanext/datarequests/actions.py:500 +msgid "This Data Request is already closed" +msgstr "Cette demande a été cloturée" + +#: ckanext/datarequests/actions.py:578 ckanext/datarequests/actions.py:671 +#: ckanext/datarequests/actions.py:717 +msgid "Comment ID has not been included" +msgstr "L'ID du commentaire n'a pas été ajouté" + +#: ckanext/datarequests/actions.py:589 ckanext/datarequests/actions.py:682 +#: ckanext/datarequests/actions.py:728 +#, python-format +msgid "Comment %s not found in the data base" +msgstr "Le commentaire %s n'a pas été trouvé" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:17 +#: ckanext/datarequests/validator.py:31 ckanext/datarequests/validator.py:34 +#: ckanext/datarequests/validator.py:41 +msgid "Title" +msgstr "Titre" + +#: ckanext/datarequests/validator.py:31 +#, python-format +msgid "Title must be a maximum of %d characters long" +msgstr "Le titre ne doit pas dépasser %d caractères" + +#: ckanext/datarequests/validator.py:34 +msgid "Title cannot be empty" +msgstr "Le titre ne peut pas être vide" + +#: ckanext/datarequests/validator.py:41 +msgid "That title is already in use" +msgstr "Ce titre est déjà utilisé" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:21 +#: ckanext/datarequests/validator.py:45 +msgid "Description" +msgstr "Description" + +#: ckanext/datarequests/validator.py:45 +#, python-format +msgid "Description must be a maximum of %d characters long" +msgstr "La description ne doit pas dépasser %d caractères" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:11 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:25 +#: ckanext/datarequests/validator.py:52 +msgid "Organization" +msgstr "Organisation" + +#: ckanext/datarequests/validator.py:52 +msgid "Organization is not valid" +msgstr "Cette organisation n'est pas valide" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:24 +#: ckanext/datarequests/validator.py:65 +msgid "Accepted Dataset" +msgstr "Jeu de données accepté" + +#: ckanext/datarequests/validator.py:65 +msgid "Dataset not found" +msgstr "Jeu de données non trouvé" + +#: ckanext/datarequests/templates/datarequests/base.html:17 +#: ckanext/datarequests/templates/datarequests/show.html:25 +#: ckanext/datarequests/validator.py:75 +msgid "Data Request" +msgstr "Demande de données" + +#: ckanext/datarequests/validator.py:75 +msgid "Data Request not found" +msgstr "Demande de données introuvable" + +#: ckanext/datarequests/validator.py:78 ckanext/datarequests/validator.py:81 +msgid "Comment" +msgstr "Commentaire" + +#: ckanext/datarequests/validator.py:78 +msgid "Comments must be a minimum of 1 character long" +msgstr "Le commentaire doit contenir au moins 1 caractère" + +#: ckanext/datarequests/validator.py:81 +#, python-format +msgid "Comments must be a maximum of %d characters long" +msgstr "Le commentaire ne doit pas dépasser %d caractères" + +#: ckanext/datarequests/controllers/ui_controller.py:129 +msgid "Newest" +msgstr "Plus récent" + +#: ckanext/datarequests/controllers/ui_controller.py:129 +msgid "Oldest" +msgstr "Plus ancien" + +#: ckanext/datarequests/controllers/ui_controller.py:145 +#: ckanext/datarequests/tests/test_ui_controller.py:628 +msgid "State" +msgstr "État" + +#: ckanext/datarequests/controllers/ui_controller.py:150 +#: ckanext/datarequests/templates/header.html:6 +#: ckanext/datarequests/tests/test_ui_controller.py:630 +msgid "Organizations" +msgstr "Organisations" + +#: ckanext/datarequests/controllers/ui_controller.py:156 +msgid "\"page\" parameter must be an integer" +msgstr "Le paramètre \"page\" doit être un nombre entier" + +#: ckanext/datarequests/controllers/ui_controller.py:159 +msgid "Unauthorized to list Data Requests" +msgstr "Vous n'êtes pas autorisé à voir les demandes de données" + +#: ckanext/datarequests/controllers/ui_controller.py:210 +msgid "Unauthorized to create a Data Request" +msgstr "Vous n'êtes pas autorisé à créer une demande de données" + +#: ckanext/datarequests/controllers/ui_controller.py:225 +#: ckanext/datarequests/controllers/ui_controller.py:248 +#: ckanext/datarequests/controllers/ui_controller.py:265 +#: ckanext/datarequests/controllers/ui_controller.py:334 +#: ckanext/datarequests/controllers/ui_controller.py:401 +#, python-format +msgid "Data Request %s not found" +msgstr "La demande %s est introuvable" + +#: ckanext/datarequests/controllers/ui_controller.py:228 +#, python-format +msgid "You are not authorized to view the Data Request %s" +msgstr "Vous n'êtes pas autorisé à voir la demande %s" + +#: ckanext/datarequests/controllers/ui_controller.py:251 +#, python-format +msgid "You are not authorized to update the Data Request %s" +msgstr "Vous n'êtes pas autorisé à mettre à jour la demande de données %s" + +#: ckanext/datarequests/controllers/ui_controller.py:261 +#: ckanext/datarequests/tests/test_ui_controller.py:672 +#, python-format +msgid "Data Request %s has been deleted" +msgstr "La demande de données %s a été supprimée" + +#: ckanext/datarequests/controllers/ui_controller.py:268 +#, python-format +msgid "You are not authorized to delete the Data Request %s" +msgstr "Vous n'êtes pas autorisé à supprimer la demande de données %s" + +#: ckanext/datarequests/controllers/ui_controller.py:316 +msgid "This data request is already closed" +msgstr "Cette demande de données est déjà cloturée" + +#: ckanext/datarequests/controllers/ui_controller.py:337 +#, python-format +msgid "You are not authorized to close the Data Request %s" +msgstr "Vous n'êtes pas autorisé à cloturer la demande de données %s" + +#: ckanext/datarequests/controllers/ui_controller.py:366 +msgid "Comment has been published" +msgstr "Le commentaire a été publié" + +#: ckanext/datarequests/controllers/ui_controller.py:368 +msgid "Comment has been updated" +msgstr "Le commentaire a été mis à jour" + +#: ckanext/datarequests/controllers/ui_controller.py:374 +#, python-format +msgid "You are not authorized to %s" +msgstr "Vous n'êtes pas autorisé à %s" + +#: ckanext/datarequests/controllers/ui_controller.py:405 +#, python-format +msgid "You are not authorized to list the comments of the Data Request %s" +msgstr "Vous n'êtes pas autorisé à voir les commentaires de la demande de données %s" + +#: ckanext/datarequests/controllers/ui_controller.py:414 +msgid "Comment has been deleted" +msgstr "Le commentaire a été supprimé" + +#: ckanext/datarequests/controllers/ui_controller.py:419 +#, python-format +msgid "Comment %s not found" +msgstr "Le commentaire %s est introuvable" + +#: ckanext/datarequests/controllers/ui_controller.py:422 +msgid "You are not authorized to delete this comment" +msgstr "Vous n'êtes pas autorisé à supprimer ce commentaire" + +#: ckanext/datarequests/templates/header.html:5 +#: ckanext/datarequests/templates/organization/read_base.html:4 +#: ckanext/datarequests/templates/user/read_base.html:4 +msgid "Datasets" +msgstr "Jeux de données" + +#: ckanext/datarequests/templates/header.html:7 +msgid "Groups" +msgstr "Groupes" + +#: ckanext/datarequests/templates/datarequests/base.html:8 +#: ckanext/datarequests/templates/datarequests/base.html:11 +#: ckanext/datarequests/templates/datarequests/close.html:6 +#: ckanext/datarequests/templates/datarequests/edit.html:6 +#: ckanext/datarequests/templates/datarequests/new.html:6 +#: ckanext/datarequests/templates/datarequests/show.html:8 +#: ckanext/datarequests/templates/header.html:8 +#: ckanext/datarequests/templates/organization/read_base.html:6 +#: ckanext/datarequests/templates/user/read_base.html:6 +msgid "Data Requests" +msgstr "Demander une donnée" + +#: ckanext/datarequests/templates/header.html:9 +#: ckanext/datarequests/templates/organization/read_base.html:7 +msgid "About" +msgstr "A propos" + +#: ckanext/datarequests/templates/datarequests/base.html:20 +msgid "" +"Data Requests allow users to ask for data that is not published in the " +"platform yet. If you want some specific data and you are not able to find" +" it among all the published datasets, you can create a new data request " +"specifying the data than you want to get." +msgstr "" +"Cette page permet aux utilisateurs de demander un jeu de données qui " +"n'est pas encore présent sur le portail. Si vous recherchez une donnée " +"en particulier et que vous n'avez pas réussi à la trouver dans les jeux " +"de données déjà présents, vous pouvez créer une demande en spécifiant " +"la donnée recherchée." + +#: ckanext/datarequests/templates/datarequests/close.html:3 +#: ckanext/datarequests/templates/datarequests/close.html:8 +#: ckanext/datarequests/templates/datarequests/close.html:12 +#: ckanext/datarequests/templates/datarequests/snippets/close_datarequest_form.html:25 +msgid "Close Data Request" +msgstr "Fermer la demande" + +#: ckanext/datarequests/templates/datarequests/comment.html:5 +#: ckanext/datarequests/templates/datarequests/show.html:28 +msgid "Comments" +msgstr "Commentaires" + +#: ckanext/datarequests/templates/datarequests/edit.html:3 +#: ckanext/datarequests/templates/datarequests/edit.html:8 +#: ckanext/datarequests/templates/datarequests/edit.html:12 +msgid "Edit Data Request" +msgstr "Modifier la demande" + +#: ckanext/datarequests/templates/datarequests/index.html:9 +#: ckanext/datarequests/templates/organization/datarequests.html:10 +msgid "Add Data Request" +msgstr "Ajouter une demande" + +#: ckanext/datarequests/templates/datarequests/index.html:12 +#: ckanext/datarequests/templates/organization/datarequests.html:13 +#: ckanext/datarequests/templates/user/datarequests.html:9 +msgid "Search Data Requests..." +msgstr "Rechercher une demande de données ..." + +#: ckanext/datarequests/templates/datarequests/new.html:3 +#: ckanext/datarequests/templates/datarequests/new.html:7 +#: ckanext/datarequests/templates/datarequests/new.html:11 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:47 +#: ckanext/datarequests/templates/datarequests/snippets/new_datarequest_form.html:7 +msgid "Create Data Request" +msgstr "Créer une demande de données" + +#: ckanext/datarequests/templates/datarequests/new.html:16 +msgid "" +"To create a data request, fill the form and specify a title and a " +"description for your request. Please, be as clear as you can in order to " +"ease the task of accomplishing your request. You can also specify an " +"organization if your data request is closely related with it. " +msgstr "" +"Pour créer une demande de donnée, remplissez le formulaire en précisant " +"le titre et en donnant une description. Merci d'être le plus clair " +"possible afin de faciliter la recherche des données. Vous pouvez " +"également spécifier une organisation si votre demande est en lien avec " +"celle-ci." + +#: ckanext/datarequests/templates/datarequests/show.html:15 +msgid "Manage" +msgstr "Gérer" + +#: ckanext/datarequests/templates/datarequests/show.html:19 +msgid "Close" +msgstr "Fermer" + +#: ckanext/datarequests/templates/datarequests/show.html:45 +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:19 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html:12 +msgid "Closed" +msgstr "Cloturé" + +#: ckanext/datarequests/templates/datarequests/show.html:50 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html:16 +msgid "Open" +msgstr "Ouvert" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:2 +msgid "Additional Info" +msgstr "Informations supplémentaires" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:7 +msgid "Creator" +msgstr "Créateur" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:8 +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:12 +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:29 +msgid "None" +msgstr "Aucun" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:15 +msgid "Created" +msgstr "Date de création" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:20 +msgid "Not closed yet" +msgstr "Non cloturé" + +#: ckanext/datarequests/templates/datarequests/snippets/close_datarequest_form.html:11 +msgid "Accep. Dataset" +msgstr "Accepter le jeu de données" + +#: ckanext/datarequests/templates/datarequests/snippets/comment_form.html:22 +msgid "Add a new Comment" +msgstr "Ajouter un nouveau commentaire" + +#: ckanext/datarequests/templates/datarequests/snippets/comment_form.html:23 +#, python-format +msgid "" +"You can use Markdown formatting here. You can refer datasets by " +"adding their URL." +msgstr "" +"Vous pouvez utiliser la mise en forme Markdown ici. Vous pouvez vous " +"référer à un jeu de données en ajoutant son URL." + +#: ckanext/datarequests/templates/datarequests/snippets/comment_form.html:28 +msgid "Cancel" +msgstr "Annuler" + +#: ckanext/datarequests/templates/datarequests/snippets/comment_form.html:29 +msgid "Update Comment" +msgstr "Mettre à jour le commentaire" + +#: ckanext/datarequests/templates/datarequests/snippets/comment_form.html:31 +msgid "Add Comment" +msgstr "Ajouter un commentaire" + +#: ckanext/datarequests/templates/datarequests/snippets/comment_item.html:18 +msgid "Are you sure you want to delete this comment?" +msgstr "Êtes-vous sûr de vouloir supprimer ce commentaire ?" + +#: ckanext/datarequests/templates/datarequests/snippets/comment_item.html:29 +msgid "commented" +msgstr "commenté" + +#: ckanext/datarequests/templates/datarequests/snippets/comments.html:3 +msgid "Current Discussion" +msgstr "Discussion actuelle" + +#: ckanext/datarequests/templates/datarequests/snippets/comments.html:13 +msgid "This data request has not been commented yet" +msgstr "Cette demande n'a pas encore été commentée" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:17 +msgid "eg. Data Request Name" +msgstr "par ex, nom de la demande de données " + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:21 +msgid "eg. Data Request description" +msgstr "par ex, description du jeu de données" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:29 +msgid "No organization" +msgstr "Aucune organisation" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:43 +msgid "Are you sure you want to delete this data request?" +msgstr "Êtes-vous sûr de vouloir supprimer cette demande ?" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:44 +msgid "Delete" +msgstr "Supprimer" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html:1 +msgid "No Data Requests found" +msgstr "Aucune demande de données trouvée" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html:14 +msgid "No Data Requests found with the given criteria" +msgstr "Aucune demande de données ne correspond aux critères renseignés " + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html:16 +msgid "How about creating one?" +msgstr "Pourquoi ne pas en créer une ?" + +#: ckanext/datarequests/templates/datarequests/snippets/edit_datarequest_form.html:4 +msgid "Update Data Request" +msgstr "Mettre à jour la demande" + +#: ckanext/datarequests/templates/home/snippets/stats.html:5 +msgid "{0} statistics" +msgstr "{0} statistiques" + +#: ckanext/datarequests/templates/home/snippets/stats.html:10 +msgid "dataset" +msgstr "jeu de données" + +#: ckanext/datarequests/templates/home/snippets/stats.html:10 +msgid "datasets" +msgstr "jeux de données" + +#: ckanext/datarequests/templates/home/snippets/stats.html:16 +msgid "organization" +msgstr "organisation" + +#: ckanext/datarequests/templates/home/snippets/stats.html:16 +msgid "organizations" +msgstr "organisations" + +#: ckanext/datarequests/templates/home/snippets/stats.html:22 +msgid "group" +msgstr "groupe" + +#: ckanext/datarequests/templates/home/snippets/stats.html:22 +msgid "groups" +msgstr "groupes" + +#: ckanext/datarequests/templates/home/snippets/stats.html:28 +msgid "related item" +msgstr "Élément lié" + +#: ckanext/datarequests/templates/home/snippets/stats.html:28 +msgid "related items" +msgstr "Éléments liés" + +#: ckanext/datarequests/templates/home/snippets/stats.html:34 +msgid "data request" +msgstr "demande de données" + +#: ckanext/datarequests/templates/home/snippets/stats.html:34 +msgid "data requests" +msgstr "demandes de données" + +#: ckanext/datarequests/templates/organization/read_base.html:5 +#: ckanext/datarequests/templates/user/read_base.html:5 +msgid "Activity Stream" +msgstr "Flux d'activité" + +#: ckanext/datarequests/templates/snippets/custom_search_form.html:6 +msgid "{number} data request found for \"{query}\"" +msgid_plural "{number} data requests found for \"{query}\"" +msgstr[0] "{number} demande trouvée pour \"{query}\"" +msgstr[1] "{number} demandes trouvées pour \"{query}\"" + +#: ckanext/datarequests/templates/snippets/custom_search_form.html:7 +msgid "No data requests found for \"{query}\"" +msgstr "Aucun jeu de données trouvé pour \"{query}\"" + +#: ckanext/datarequests/templates/snippets/custom_search_form.html:8 +msgid "{number} data request found" +msgid_plural "{number} data requests found" +msgstr[0] "{number} jeu de données trouvé" +msgstr[1] "{number} jeux de données trouvés" + +#: ckanext/datarequests/templates/snippets/custom_search_form.html:9 +msgid "No data requests found" +msgstr "Aucune demande trouvée" + diff --git a/ckanext/datarequests/i18n/ro/LC_MESSAGES/ckanext-datarequests.mo b/ckanext/datarequests/i18n/ro/LC_MESSAGES/ckanext-datarequests.mo new file mode 100644 index 00000000..a84442be Binary files /dev/null and b/ckanext/datarequests/i18n/ro/LC_MESSAGES/ckanext-datarequests.mo differ diff --git a/ckanext/datarequests/i18n/ro/LC_MESSAGES/ckanext-datarequests.po b/ckanext/datarequests/i18n/ro/LC_MESSAGES/ckanext-datarequests.po new file mode 100644 index 00000000..26672888 --- /dev/null +++ b/ckanext/datarequests/i18n/ro/LC_MESSAGES/ckanext-datarequests.po @@ -0,0 +1,508 @@ +# Romanian translations for ckanext-datarequests. +# Copyright (C) 2016 ORGANIZATION +# This file is distributed under the same license as the +# ckanext-datarequests +# project. +# FIRST AUTHOR , 2016. +# +msgid "" +msgstr "" +"Project-Id-Version: ckanext-datarequests 0.3.1\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2016-04-28 14:45+0200\n" +"PO-Revision-Date: 2019-12-14 15:49+0200\n" +"Language: ro\n" +"Language-Team: ro \n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 " +"< 20)) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.3.4\n" +"Last-Translator: \n" +"X-Generator: Poedit 2.2.4\n" + +#: ckanext/datarequests/actions.py:192 ckanext/datarequests/actions.py:244 +#: ckanext/datarequests/actions.py:435 ckanext/datarequests/actions.py:480 +#: ckanext/datarequests/actions.py:536 ckanext/datarequests/actions.py:624 +msgid "Data Request ID has not been included" +msgstr "ID-ul cererii nu a fost inclus" + +#: ckanext/datarequests/actions.py:203 ckanext/datarequests/actions.py:255 +#: ckanext/datarequests/actions.py:446 ckanext/datarequests/actions.py:491 +#, python-format +msgid "Data Request %s not found in the data base" +msgstr "Cererea %s nu a fost găsită in baza de date" + +#: ckanext/datarequests/actions.py:500 +msgid "This Data Request is already closed" +msgstr "Această cerere a fost deja închisă" + +#: ckanext/datarequests/actions.py:578 ckanext/datarequests/actions.py:671 +#: ckanext/datarequests/actions.py:717 +msgid "Comment ID has not been included" +msgstr "ID-ul comentariului nu a fost inclus" + +#: ckanext/datarequests/actions.py:589 ckanext/datarequests/actions.py:682 +#: ckanext/datarequests/actions.py:728 +#, python-format +msgid "Comment %s not found in the data base" +msgstr "Comentariul %s nu a fost găsit in baza de date" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:17 +#: ckanext/datarequests/validator.py:31 ckanext/datarequests/validator.py:34 +#: ckanext/datarequests/validator.py:41 +msgid "Title" +msgstr "Titlu" + +#: ckanext/datarequests/validator.py:31 +#, python-format +msgid "Title must be a maximum of %d characters long" +msgstr "Titlul trebuie sa aibe maxim %d caracterre" + +#: ckanext/datarequests/validator.py:34 +msgid "Title cannot be empty" +msgstr "Titlu nu poate fi nul" + +#: ckanext/datarequests/validator.py:41 +msgid "That title is already in use" +msgstr "Aceest titlu este deja folosit" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:21 +#: ckanext/datarequests/validator.py:45 +msgid "Description" +msgstr "Descriere" + +#: ckanext/datarequests/validator.py:45 +#, python-format +msgid "Description must be a maximum of %d characters long" +msgstr "Descrierea trebuie sa aibe maxim %d caractere" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:11 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:25 +#: ckanext/datarequests/validator.py:52 +msgid "Organization" +msgstr "Organizație" + +#: ckanext/datarequests/validator.py:52 +msgid "Organization is not valid" +msgstr "Organizația un este validă" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:24 +#: ckanext/datarequests/validator.py:65 +msgid "Accepted Dataset" +msgstr "Set de date acceptat" + +#: ckanext/datarequests/validator.py:65 +msgid "Dataset not found" +msgstr "Setul de date nu a fost găsit" + +#: ckanext/datarequests/templates/datarequests/base.html:17 +#: ckanext/datarequests/templates/datarequests/show.html:25 +#: ckanext/datarequests/validator.py:75 +msgid "Data Request" +msgstr "Cerere de Date" + +#: ckanext/datarequests/validator.py:75 +msgid "Data Request not found" +msgstr "Cererea de date nu a fost găsită" + +#: ckanext/datarequests/validator.py:78 ckanext/datarequests/validator.py:81 +msgid "Comment" +msgstr "Comentariu" + +#: ckanext/datarequests/validator.py:78 +msgid "Comments must be a minimum of 1 character long" +msgstr "Comentariile trebuie să aibe minim un caracter" + +#: ckanext/datarequests/validator.py:81 +#, python-format +msgid "Comments must be a maximum of %d characters long" +msgstr "Comentariile trebuie sa aibe maxim %d caractere" + +#: ckanext/datarequests/controllers/ui_controller.py:129 +msgid "Newest" +msgstr "Cele mai recente" + +#: ckanext/datarequests/controllers/ui_controller.py:129 +msgid "Oldest" +msgstr "Cele mai vechi" + +#: ckanext/datarequests/controllers/ui_controller.py:145 +#: ckanext/datarequests/tests/test_ui_controller.py:628 +msgid "State" +msgstr "Stare" + +#: ckanext/datarequests/controllers/ui_controller.py:150 +#: ckanext/datarequests/templates/header.html:6 +#: ckanext/datarequests/tests/test_ui_controller.py:630 +msgid "Organizations" +msgstr "Organizații" + +#: ckanext/datarequests/controllers/ui_controller.py:156 +msgid "\"page\" parameter must be an integer" +msgstr "parametrul “page” trebuie sa fie de tip integer" + +#: ckanext/datarequests/controllers/ui_controller.py:159 +msgid "Unauthorized to list Data Requests" +msgstr "Neautorizat pentru listarea de cereri de date" + +#: ckanext/datarequests/controllers/ui_controller.py:210 +msgid "Unauthorized to create a Data Request" +msgstr "Neautorizat pentru crearea de cereri de date" + +#: ckanext/datarequests/controllers/ui_controller.py:225 +#: ckanext/datarequests/controllers/ui_controller.py:248 +#: ckanext/datarequests/controllers/ui_controller.py:265 +#: ckanext/datarequests/controllers/ui_controller.py:334 +#: ckanext/datarequests/controllers/ui_controller.py:401 +#, python-format +msgid "Data Request %s not found" +msgstr "Cererea de date %s nu a fost găsită" + +#: ckanext/datarequests/controllers/ui_controller.py:228 +#, python-format +msgid "You are not authorized to view the Data Request %s" +msgstr "Nu ești autorizat să vizualizezi cererea de date %s" + +#: ckanext/datarequests/controllers/ui_controller.py:251 +#, python-format +msgid "You are not authorized to update the Data Request %s" +msgstr "Nu ești autorizat să actualizezi cererea de date %s" + +#: ckanext/datarequests/controllers/ui_controller.py:261 +#: ckanext/datarequests/tests/test_ui_controller.py:672 +#, python-format +msgid "Data Request %s has been deleted" +msgstr "Cererea de date %s a fost ștearsă" + +#: ckanext/datarequests/controllers/ui_controller.py:268 +#, python-format +msgid "You are not authorized to delete the Data Request %s" +msgstr "Nu ești autorizat să ștergi cererea de date %s" + +#: ckanext/datarequests/controllers/ui_controller.py:316 +msgid "This data request is already closed" +msgstr "Cererea este deja închisă" + +#: ckanext/datarequests/controllers/ui_controller.py:337 +#, python-format +msgid "You are not authorized to close the Data Request %s" +msgstr "Nu ești autorizat sa închizi cererea de date %s" + +#: ckanext/datarequests/controllers/ui_controller.py:366 +msgid "Comment has been published" +msgstr "Comentariul a fost publicat" + +#: ckanext/datarequests/controllers/ui_controller.py:368 +msgid "Comment has been updated" +msgstr "Comentariul a fost actualizat" + +#: ckanext/datarequests/controllers/ui_controller.py:374 +#, python-format +msgid "You are not authorized to %s" +msgstr "Nu ești autorizat să %s" + +#: ckanext/datarequests/controllers/ui_controller.py:405 +#, python-format +msgid "You are not authorized to list the comments of the Data Request %s" +msgstr "Nu ești autorizat să listezi comentariile pentru cererea %s" + +#: ckanext/datarequests/controllers/ui_controller.py:414 +msgid "Comment has been deleted" +msgstr "Comentariul a fost șters" + +#: ckanext/datarequests/controllers/ui_controller.py:419 +#, python-format +msgid "Comment %s not found" +msgstr "Comentariul %s nu a fost găsit" + +#: ckanext/datarequests/controllers/ui_controller.py:422 +msgid "You are not authorized to delete this comment" +msgstr "Nu ești autorizat să ștergi acest comentariu" + +#: ckanext/datarequests/templates/header.html:5 +#: ckanext/datarequests/templates/organization/read_base.html:4 +#: ckanext/datarequests/templates/user/read_base.html:4 +msgid "Datasets" +msgstr "Seturi de date" + +#: ckanext/datarequests/templates/header.html:7 +msgid "Groups" +msgstr "Grupuri" + +#: ckanext/datarequests/templates/datarequests/base.html:8 +#: ckanext/datarequests/templates/datarequests/base.html:11 +#: ckanext/datarequests/templates/datarequests/close.html:6 +#: ckanext/datarequests/templates/datarequests/edit.html:6 +#: ckanext/datarequests/templates/datarequests/new.html:6 +#: ckanext/datarequests/templates/datarequests/show.html:8 +#: ckanext/datarequests/templates/header.html:8 +#: ckanext/datarequests/templates/organization/read_base.html:6 +#: ckanext/datarequests/templates/user/read_base.html:6 +msgid "Data Requests" +msgstr "Cereri de date" + +#: ckanext/datarequests/templates/header.html:9 +#: ckanext/datarequests/templates/organization/read_base.html:7 +msgid "About" +msgstr "Despree" + +#: ckanext/datarequests/templates/datarequests/base.html:20 +msgid "" +"Data Requests allow users to ask for data that is not published in the " +"platform yet. If you want some specific data and you are not able to find " +"it among all the published datasets, you can create a new data request " +"specifying the data than you want to get." +msgstr "" +"Cererile de date permit utilizatorilor să ceara date care încă nu au fost " +"publicate pe acest portal încă. Dacă dorești un set de date pe care nu ai " +"reușit să îl găsești, poți crea o nouă cerere specificând datele pe care " +"le dorești." + +#: ckanext/datarequests/templates/datarequests/close.html:3 +#: ckanext/datarequests/templates/datarequests/close.html:8 +#: ckanext/datarequests/templates/datarequests/close.html:12 +#: ckanext/datarequests/templates/datarequests/snippets/close_datarequest_form.html:25 +msgid "Close Data Request" +msgstr "Închide cererea de date" + +#: ckanext/datarequests/templates/datarequests/comment.html:5 +#: ckanext/datarequests/templates/datarequests/show.html:28 +msgid "Comments" +msgstr "Comentarii" + +#: ckanext/datarequests/templates/datarequests/edit.html:3 +#: ckanext/datarequests/templates/datarequests/edit.html:8 +#: ckanext/datarequests/templates/datarequests/edit.html:12 +msgid "Edit Data Request" +msgstr "Editează cererea" + +#: ckanext/datarequests/templates/datarequests/index.html:9 +#: ckanext/datarequests/templates/organization/datarequests.html:10 +msgid "Add Data Request" +msgstr "Adaugă cerere" + +#: ckanext/datarequests/templates/datarequests/index.html:12 +#: ckanext/datarequests/templates/organization/datarequests.html:13 +#: ckanext/datarequests/templates/user/datarequests.html:9 +msgid "Search Data Requests..." +msgstr "Caută cereri..." + +#: ckanext/datarequests/templates/datarequests/new.html:3 +#: ckanext/datarequests/templates/datarequests/new.html:7 +#: ckanext/datarequests/templates/datarequests/new.html:11 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:47 +#: ckanext/datarequests/templates/datarequests/snippets/new_datarequest_form.html:7 +msgid "Create Data Request" +msgstr "Crează cerere" + +#: ckanext/datarequests/templates/datarequests/new.html:16 +msgid "" +"To create a data request, fill the form and specify a title and a " +"description for your request. Please, be as clear as you can in order to " +"ease the task of accomplishing your request. You can also specify an " +"organization if your data request is closely related with it. " +msgstr "" +"Pentru a crea o nouă cerere te rugăm sa completezi formularul, " +"specificând un titlu și o descriere a datelor pe care le dorești. Te " +"rugăm sa fii cât mai explicit, pentru a putea să ușurăm procesul de " +"obținere a datelor. De asemenea, poți selecta organizația care consideri " +"că este în măsura să ofere respectivele date. " + +#: ckanext/datarequests/templates/datarequests/show.html:15 +msgid "Manage" +msgstr "Administrează" + +#: ckanext/datarequests/templates/datarequests/show.html:19 +msgid "Close" +msgstr "Închide" + +#: ckanext/datarequests/templates/datarequests/show.html:45 +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:19 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html:12 +msgid "Closed" +msgstr "Închis" + +#: ckanext/datarequests/templates/datarequests/show.html:50 +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html:16 +msgid "Open" +msgstr "Deschis" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:2 +msgid "Additional Info" +msgstr "Informații suplimentare" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:7 +msgid "Creator" +msgstr "Creator" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:8 +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:12 +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:29 +msgid "None" +msgstr "Gol" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:15 +msgid "Created" +msgstr "Creat" + +#: ckanext/datarequests/templates/datarequests/snippets/additional_info.html:20 +msgid "Not closed yet" +msgstr "Neînchis încă" + +#: ckanext/datarequests/templates/datarequests/snippets/close_datarequest_form.html:11 +msgid "Accep. Dataset" +msgstr "Acceptă. Set de date" + +#: ckanext/datarequests/templates/datarequests/snippets/comment_form.html:22 +msgid "Add a new Comment" +msgstr "Adaugă comentariu nou" + +#: ckanext/datarequests/templates/datarequests/snippets/comment_form.html:23 +#, python-format +msgid "" +"You can use Markdown formatting here. You can refer datasets by adding their " +"URL." +msgstr "" +"Poți folosi formatarea Markdown aici. Poți refenția seturi de date " +"adăugând URL-ul lor." + +#: ckanext/datarequests/templates/datarequests/snippets/comment_form.html:28 +msgid "Cancel" +msgstr "Întrerupe" + +#: ckanext/datarequests/templates/datarequests/snippets/comment_form.html:29 +msgid "Update Comment" +msgstr "Actualizează comentariu" + +#: ckanext/datarequests/templates/datarequests/snippets/comment_form.html:31 +msgid "Add Comment" +msgstr "Adaugă comentariu" + +#: ckanext/datarequests/templates/datarequests/snippets/comment_item.html:18 +msgid "Are you sure you want to delete this comment?" +msgstr "Sigur dorești să ștergi acest comentariu?" + +#: ckanext/datarequests/templates/datarequests/snippets/comment_item.html:29 +msgid "commented" +msgstr "comentat" + +#: ckanext/datarequests/templates/datarequests/snippets/comments.html:3 +msgid "Current Discussion" +msgstr "Discuția curentă" + +#: ckanext/datarequests/templates/datarequests/snippets/comments.html:13 +msgid "This data request has not been commented yet" +msgstr "Această cerere nu are niciun comentariu" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:17 +msgid "eg. Data Request Name" +msgstr "ex: Numele cererii de date" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:21 +msgid "eg. Data Request description" +msgstr "ex: Descrierea cererii de date" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:29 +msgid "No organization" +msgstr "Nicio organizație" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:43 +msgid "Are you sure you want to delete this data request?" +msgstr "Ești sigur că dorești să ștergi această cerere?" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_form.html:44 +msgid "Delete" +msgstr "Șterge" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html:1 +msgid "No Data Requests found" +msgstr "Nicio cerere găsită" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html:14 +msgid "No Data Requests found with the given criteria" +msgstr "Nicio cerere nu a fost găsită pentru acest criteriu" + +#: ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html:16 +msgid "How about creating one?" +msgstr "Vrei sa creezi una?" + +#: ckanext/datarequests/templates/datarequests/snippets/edit_datarequest_form.html:4 +msgid "Update Data Request" +msgstr "Actualizează cererea de date" + +#: ckanext/datarequests/templates/home/snippets/stats.html:5 +msgid "{0} statistics" +msgstr "{0} statistici" + +#: ckanext/datarequests/templates/home/snippets/stats.html:10 +msgid "dataset" +msgstr "set de date" + +#: ckanext/datarequests/templates/home/snippets/stats.html:10 +msgid "datasets" +msgstr "seturi de date" + +#: ckanext/datarequests/templates/home/snippets/stats.html:16 +msgid "organization" +msgstr "organizație" + +#: ckanext/datarequests/templates/home/snippets/stats.html:16 +msgid "organizations" +msgstr "organizații" + +#: ckanext/datarequests/templates/home/snippets/stats.html:22 +msgid "group" +msgstr "grup" + +#: ckanext/datarequests/templates/home/snippets/stats.html:22 +msgid "groups" +msgstr "grupuri" + +#: ckanext/datarequests/templates/home/snippets/stats.html:28 +msgid "related item" +msgstr "item asemănător" + +#: ckanext/datarequests/templates/home/snippets/stats.html:28 +msgid "related items" +msgstr "itemi asemănători" + +#: ckanext/datarequests/templates/home/snippets/stats.html:34 +msgid "data request" +msgstr "cerere de date" + +#: ckanext/datarequests/templates/home/snippets/stats.html:34 +msgid "data requests" +msgstr "cereri de date" + +#: ckanext/datarequests/templates/organization/read_base.html:5 +#: ckanext/datarequests/templates/user/read_base.html:5 +msgid "Activity Stream" +msgstr "Flux de activitate" + +#: ckanext/datarequests/templates/snippets/custom_search_form.html:6 +msgid "{number} data request found for \"{query}\"" +msgid_plural "{number} data requests found for \"{query}\"" +msgstr[0] "{number} cerere găsită pentru “{query}”" +msgstr[1] "{number} cereri găsite pentru “{query}”" +msgstr[2] "{number} cereri găsite pentru “{query}”" + +#: ckanext/datarequests/templates/snippets/custom_search_form.html:7 +msgid "No data requests found for \"{query}\"" +msgstr "Nicio cerere nu a fost găsită pentru “{query}”" + +#: ckanext/datarequests/templates/snippets/custom_search_form.html:8 +msgid "{number} data request found" +msgid_plural "{number} data requests found" +msgstr[0] "{number} cereri găsită" +msgstr[1] "{number} cereri găsite" +msgstr[2] "{number} cereri găsite" + +#: ckanext/datarequests/templates/snippets/custom_search_form.html:9 +msgid "No data requests found" +msgstr "Nicio cere nu a fost găsită" diff --git a/ckanext/datarequests/i18n/so/LC_MESSAGES/ckanext-datarequests.mo b/ckanext/datarequests/i18n/so/LC_MESSAGES/ckanext-datarequests.mo new file mode 100644 index 00000000..40591a75 Binary files /dev/null and b/ckanext/datarequests/i18n/so/LC_MESSAGES/ckanext-datarequests.mo differ diff --git a/ckanext/datarequests/plugin.py b/ckanext/datarequests/plugin/__init__.py similarity index 56% rename from ckanext/datarequests/plugin.py rename to ckanext/datarequests/plugin/__init__.py index d2c78c3a..c6f0e1d9 100644 --- a/ckanext/datarequests/plugin.py +++ b/ckanext/datarequests/plugin/__init__.py @@ -17,45 +17,27 @@ # You should have received a copy of the GNU Affero General Public License # along with CKAN Data Requests Extension. If not, see . -import ckan.lib.helpers as h -import ckan.plugins as p -import ckan.plugins.toolkit as tk -import auth -import actions -import constants -import helpers import os +import six import sys -from functools import partial -from pylons import config - - -def get_config_bool_value(config_name, default_value=False): - value = config.get(config_name, default_value) - value = value if type(value) == bool else value != 'False' - return value - -def is_fontawesome_4(): - if hasattr(h, 'ckan_version'): - ckan_version = float(h.ckan_version()[0:3]) - return ckan_version >= 2.7 - else: - return False +import ckan.plugins as p +import ckan.plugins.toolkit as tk +from ckanext.datarequests import auth, actions, common, constants, helpers -def get_plus_icon(): - return 'plus-square' if is_fontawesome_4() else 'plus-sign-alt' +from functools import partial -def get_question_icon(): - return 'question-circle' if is_fontawesome_4() else 'question-sign' +if tk.check_ckan_version("2.9"): + from .flask_plugin import MixinPlugin +else: + from .pylons_plugin import MixinPlugin -class DataRequestsPlugin(p.SingletonPlugin): +class DataRequestsPlugin(MixinPlugin, p.SingletonPlugin): p.implements(p.IActions) p.implements(p.IAuthFunctions) p.implements(p.IConfigurer) - p.implements(p.IRoutes, inherit=True) p.implements(p.ITemplateHelpers) # ITranslation only available in 2.5+ @@ -65,9 +47,11 @@ class DataRequestsPlugin(p.SingletonPlugin): pass def __init__(self, name=None): - self.comments_enabled = get_config_bool_value('ckan.datarequests.comments', True) - self._show_datarequests_badge = get_config_bool_value('ckan.datarequests.show_datarequests_badge') + self.comments_enabled = common.get_config_bool_value('ckan.datarequests.comments', True) + self._show_datarequests_badge = common.get_config_bool_value('ckan.datarequests.show_datarequests_badge') self.name = 'datarequests' + self.is_description_required = common.get_config_bool_value('ckan.datarequests.description_required', False) + self.closing_circumstances_enabled = common.get_config_bool_value('ckan.datarequests.enable_closing_circumstances', False) ###################################################################### ############################## IACTIONS ############################## @@ -126,82 +110,22 @@ def get_auth_functions(self): 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. - tk.add_template_directory(config, 'templates') + tk.add_template_directory(config, '../templates') # Register this plugin's fanstatic directory with CKAN. - tk.add_public_directory(config, 'public') + tk.add_public_directory(config, '../public') # Register this plugin's fanstatic directory with CKAN. - tk.add_resource('fanstatic', 'datarequest') - - ###################################################################### - ############################## IROUTES ############################### - ###################################################################### - - def before_map(self, m): - # Data Requests index - m.connect('datarequests_index', "/%s" % constants.DATAREQUESTS_MAIN_PATH, - controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='index', conditions=dict(method=['GET'])) - - # Create a Data Request - m.connect('/%s/new' % constants.DATAREQUESTS_MAIN_PATH, - controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='new', conditions=dict(method=['GET', 'POST'])) - - # Show a Data Request - m.connect('show_datarequest', '/%s/{id}' % constants.DATAREQUESTS_MAIN_PATH, - controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='show', conditions=dict(method=['GET']), ckan_icon=get_question_icon()) - - # Update a Data Request - m.connect('/%s/edit/{id}' % constants.DATAREQUESTS_MAIN_PATH, - controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='update', conditions=dict(method=['GET', 'POST'])) - - # Delete a Data Request - m.connect('/%s/delete/{id}' % constants.DATAREQUESTS_MAIN_PATH, - controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='delete', conditions=dict(method=['POST'])) - - # Close a Data Request - m.connect('/%s/close/{id}' % constants.DATAREQUESTS_MAIN_PATH, - controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='close', conditions=dict(method=['GET', 'POST'])) - - # Data Request that belongs to an organization - m.connect('organization_datarequests', '/organization/%s/{id}' % constants.DATAREQUESTS_MAIN_PATH, - controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='organization_datarequests', conditions=dict(method=['GET']), - ckan_icon=get_question_icon()) - - # Data Request that belongs to an user - m.connect('user_datarequests', '/user/%s/{id}' % constants.DATAREQUESTS_MAIN_PATH, - controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='user_datarequests', conditions=dict(method=['GET']), - ckan_icon=get_question_icon()) - - # Follow & Unfollow - m.connect('/%s/follow/{id}' % constants.DATAREQUESTS_MAIN_PATH, - controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='follow', conditions=dict(method=['POST'])) - - m.connect('/%s/unfollow/{id}' % constants.DATAREQUESTS_MAIN_PATH, - controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='unfollow', conditions=dict(method=['POST'])) - - if self.comments_enabled: - # Comment, update and view comments (of) a Data Request - m.connect('comment_datarequest', '/%s/comment/{id}' % constants.DATAREQUESTS_MAIN_PATH, - controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='comment', conditions=dict(method=['GET', 'POST']), ckan_icon='comment') - - # Delete data request - m.connect('/%s/comment/{datarequest_id}/delete/{comment_id}' % constants.DATAREQUESTS_MAIN_PATH, - controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', - action='delete_comment', conditions=dict(method=['GET', 'POST'])) + tk.add_resource('../fanstatic', 'datarequest') - return m + def update_config_schema(self, schema): + if self.closing_circumstances_enabled: + ignore_missing = tk.get_validator('ignore_missing') + schema.update({ + # This is a custom configuration option + 'ckan.datarequests.closing_circumstances': [ignore_missing, six.text_type], + }) + return schema ###################################################################### ######################### ITEMPLATESHELPER ########################### @@ -214,8 +138,12 @@ def get_helpers(self): 'get_comments_badge': helpers.get_comments_badge, 'get_open_datarequests_number': helpers.get_open_datarequests_number, 'get_open_datarequests_badge': partial(helpers.get_open_datarequests_badge, self._show_datarequests_badge), - 'get_plus_icon': get_plus_icon, - 'is_following_datarequest': helpers.is_following_datarequest + 'get_plus_icon': common.get_plus_icon, + 'get_question_icon': common.get_question_icon, + 'is_following_datarequest': helpers.is_following_datarequest, + 'is_description_required': self.is_description_required, + 'closing_circumstances_enabled': self.closing_circumstances_enabled, + 'get_closing_circumstances': helpers.get_closing_circumstances } ###################################################################### @@ -236,7 +164,7 @@ def i18n_directory(self): # assume plugin is called ckanext..<...>.PluginClass extension_module_name = '.'.join(self.__module__.split('.')[:3]) module = sys.modules[extension_module_name] - return os.path.join(os.path.dirname(module.__file__), 'i18n') + return os.path.join(os.path.dirname(module.__file__), '..', 'i18n') def i18n_locales(self): '''Change the list of locales that this plugin handles @@ -245,10 +173,10 @@ def i18n_locales(self): plugin ''' directory = self.i18n_directory() - return [ d for - d in os.listdir(directory) - if os.path.isdir(os.path.join(directory, d)) - ] + return [d for + d in os.listdir(directory) + if os.path.isdir(os.path.join(directory, d)) + ] def i18n_domain(self): '''Change the gettext domain handled by this plugin diff --git a/ckanext/datarequests/plugin/flask_plugin.py b/ckanext/datarequests/plugin/flask_plugin.py new file mode 100644 index 00000000..822f3583 --- /dev/null +++ b/ckanext/datarequests/plugin/flask_plugin.py @@ -0,0 +1,104 @@ +# encoding: utf-8 + +import ckan.plugins as p +from flask import Blueprint + +from ckanext.datarequests import constants +from ckanext.datarequests.controllers import controller_functions + + +datarequests_bp = Blueprint("datarequest", __name__) + + +class MixinPlugin(p.SingletonPlugin): + p.implements(p.IBlueprint) + + # IBlueprint + + def get_blueprint(self): + rules = [ + ( + "/" + constants.DATAREQUESTS_MAIN_PATH, + "index", + controller_functions.index, + ('GET',), + ), + ( + "/{}/new".format(constants.DATAREQUESTS_MAIN_PATH), + "new", + controller_functions.new, + ('GET', 'POST',), + ), + ( + "/{}/".format(constants.DATAREQUESTS_MAIN_PATH), + "show", + controller_functions.show, + ('GET',), + ), + ( + "/{}/edit/".format(constants.DATAREQUESTS_MAIN_PATH), + "update", + controller_functions.update, + ('GET', 'POST',), + ), + ( + "/{}/delete/".format(constants.DATAREQUESTS_MAIN_PATH), + "delete", + controller_functions.delete, + ('POST',), + ), + ( + "/{}/close/".format(constants.DATAREQUESTS_MAIN_PATH), + "close", + controller_functions.close, + ('GET', 'POST',), + ), + ( + "/{}/follow/".format(constants.DATAREQUESTS_MAIN_PATH), + "follow", + controller_functions.follow, + ('POST',), + ), + ( + "/{}/unfollow/".format(constants.DATAREQUESTS_MAIN_PATH), + "unfollow", + controller_functions.unfollow, + ('POST',), + ), + ( + "/organization/{}/".format(constants.DATAREQUESTS_MAIN_PATH), + "organization", + controller_functions.organization, + ('GET',), + ), + ( + "/user/{}/".format(constants.DATAREQUESTS_MAIN_PATH), + "user", + controller_functions.user, + ('GET',), + ), + ] + + if self.comments_enabled: + rules.extend( + [ + ( + "/{}/comment/".format(constants.DATAREQUESTS_MAIN_PATH), + "comment", + controller_functions.comment, + ('GET', 'POST',), + ), + ( + "/{}/comment//delete/".format( + constants.DATAREQUESTS_MAIN_PATH), + "delete_comment", + controller_functions.delete_comment, + ('GET', 'POST',), + ), + ] + ) + + for rule in rules: + datarequests_bp.add_url_rule(rule[0], endpoint=rule[1], view_func=rule[2], methods=rule[3]) + + return [datarequests_bp] diff --git a/ckanext/datarequests/plugin/pylons_plugin.py b/ckanext/datarequests/plugin/pylons_plugin.py new file mode 100644 index 00000000..2bdd9a0a --- /dev/null +++ b/ckanext/datarequests/plugin/pylons_plugin.py @@ -0,0 +1,84 @@ +# encoding: utf-8 + +import ckan.plugins as p + +from ckanext.datarequests import common, constants + + +class MixinPlugin(p.SingletonPlugin): + p.implements(p.IRoutes, inherit=True) + + ###################################################################### + ############################## IROUTES ############################### + ###################################################################### + + def before_map(self, mapper): + from routes.mapper import SubMapper + controller_map = SubMapper( + mapper, controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI') + + m = SubMapper(controller_map, path_prefix="/" + constants.DATAREQUESTS_MAIN_PATH) + + # Data Requests index + m.connect('datarequests_index', '', action='index', conditions={'method': ['GET']}) + m.connect('datarequest.index', '', action='index', conditions={'method': ['GET']}) + + # Create a Data Request + m.connect('datarequest.new', '/new', action='new', conditions={'method': ['GET', 'POST']}) + + # Show a Data Request + m.connect('show_datarequest', '/{id}', + action='show', conditions={'method': ['GET']}, ckan_icon=common.get_question_icon()) + m.connect('datarequest.show', '/{id}', + action='show', conditions={'method': ['GET']}, ckan_icon=common.get_question_icon()) + + # Update a Data Request + m.connect('datarequest.update', '/edit/{id}', + action='update', conditions={'method': ['GET', 'POST']}) + + # Delete a Data Request + m.connect('datarequest.delete', '/delete/{id}', + action='delete', conditions={'method': ['POST']}) + + # Close a Data Request + m.connect('datarequest.close', '/close/{id}', + action='close', conditions={'method': ['GET', 'POST']}) + + # Follow & Unfollow + m.connect('datarequest.follow', '/follow/{id}', + action='follow', conditions={'method': ['POST']}) + + m.connect('datarequest.unfollow', '/unfollow/{id}', + action='unfollow', conditions={'method': ['POST']}) + + if self.comments_enabled: + # Comment, update and view comments (of) a Data Request + m.connect('comment_datarequest', '/comment/{id}', + action='comment', conditions={'method': ['GET', 'POST']}, ckan_icon='comment') + m.connect('datarequest.comment', '/comment/{id}', + action='comment', conditions={'method': ['GET', 'POST']}, ckan_icon='comment') + + # Delete data request + m.connect('datarequest.delete_comment', '/comment/{datarequest_id}/delete/{comment_id}', + action='delete_comment', conditions={'method': ['GET', 'POST']}) + + list_datarequests_map = SubMapper( + controller_map, conditions={'method': ['GET']}, ckan_icon=common.get_question_icon()) + + # Data Requests that belong to an organization + list_datarequests_map.connect( + 'organization_datarequests', '/organization/%s/{id}' % constants.DATAREQUESTS_MAIN_PATH, + action='organization_datarequests') + list_datarequests_map.connect( + 'datarequest.organization', '/organization/%s/{id}' % constants.DATAREQUESTS_MAIN_PATH, + action='organization_datarequests') + + # Data Requests that belong to a user + list_datarequests_map.connect( + 'user_datarequests', '/user/%s/{id}' % constants.DATAREQUESTS_MAIN_PATH, + action='user_datarequests') + list_datarequests_map.connect( + 'datarequest.user', '/user/%s/{id}' % constants.DATAREQUESTS_MAIN_PATH, + action='user_datarequests') + + return mapper diff --git a/ckanext/datarequests/request_helpers.py b/ckanext/datarequests/request_helpers.py new file mode 100644 index 00000000..b878df22 --- /dev/null +++ b/ckanext/datarequests/request_helpers.py @@ -0,0 +1,37 @@ +# encoding: utf-8 +""" Some useful functions for interacting with the current request. +""" + +from ckan.common import request + + +def get_cookie(field_name, default=None): + """ Get the value of a cookie, or the default value if not present. + """ + return request.cookies.get(field_name, default) + + +def get_query_params(): + return getattr(request, 'GET', None) or getattr(request, 'args', {}) + + +def get_post_params(): + return getattr(request, 'POST', None) or getattr(request, 'form', {}) + + +def get_first_post_param(field_name, default=None): + """ Retrieve the first POST parameter with the specified name + for the current request. + + This uses 'request.POST' for Pylons and 'request.form' for Flask. + """ + return get_post_params().get(field_name, default) + + +def get_first_query_param(field_name, default=None): + """ Retrieve the first GET parameter with the specified name + for the current request. + + This uses 'request.GET' for Pylons and 'request.args' for Flask. + """ + return get_query_params().get(field_name, default) diff --git a/ckanext/datarequests/templates/admin/config.html b/ckanext/datarequests/templates/admin/config.html new file mode 100644 index 00000000..6a4016bc --- /dev/null +++ b/ckanext/datarequests/templates/admin/config.html @@ -0,0 +1,27 @@ +{% ckan_extends %} + +{% import 'macros/form.html' as form %} + +{% block admin_form %} + + {{ super() }} + {% if h.closing_circumstances_enabled %} + {{ form.textarea('ckan.datarequests.closing_circumstances', id='field-ckan.datarequests.closing_circumstances', label=_('Data request closing circumstances'), placeholder=_('eg.\nPartially released|nominate_dataset\nTo be released as open data at a later date|nominate_approximate_date\nData openly available elsewhere'), value=data['ckan.datarequests.closing_circumstances'], error=errors['ckan.datarequests.closing_circumstances']) }} + {% endif %} +{% endblock %} + + +{% block admin_form_help %} + + {{ super() }} + {% if h.closing_circumstances_enabled %} +

+ Data Request Closing Circumstances: + Options displayed when closing a data request
+ Optional conditions are added to an option by adding a pipe character (|) followed by the condition
+ The following conditions can be used:
+ - nominate_dataset (e.g. Released as open data|nominate_dataset)
+ - nominate_approximate_date (e.g.To be released as open data at a later date|nominate_approximate_date) +

+ {% endif %} +{% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/base.html b/ckanext/datarequests/templates/datarequests/base.html index 314e603b..178f143e 100644 --- a/ckanext/datarequests/templates/datarequests/base.html +++ b/ckanext/datarequests/templates/datarequests/base.html @@ -8,7 +8,7 @@ {% block subtitle %}{{ _('Data Requests') }}{% endblock %} {% block breadcrumb_content %} -
  • {% link_for _('Data Requests'), controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='index' %}
  • +
  • {% link_for _('Data Requests'), named_route='datarequest.index' %}
  • {% endblock %} {% block secondary_content %} @@ -23,5 +23,5 @@

    {% tr {% endblock %} - + {% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/close.html b/ckanext/datarequests/templates/datarequests/close.html index 945ba87e..5716c78b 100644 --- a/ckanext/datarequests/templates/datarequests/close.html +++ b/ckanext/datarequests/templates/datarequests/close.html @@ -3,8 +3,8 @@ {% block subtitle %}{{ _('Close Data Request') }}{% endblock %} {% block breadcrumb_content %} -
  • {% link_for _('Data Requests'), controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='index' %}
  • -
  • {% link_for c.datarequest.get('title')|truncate(30), controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='show', id=c.datarequest.get('id') %}
  • +
  • {% link_for _('Data Requests'), named_route='datarequest.index' %}
  • +
  • {% link_for c.datarequest.get('title')|truncate(30), named_route='datarequest.show', id=c.datarequest.get('id') %}
  • {{ _('Close Data Request') }}
  • {% endblock %} @@ -13,4 +13,4 @@

    {% block pa {% snippet "datarequests/snippets/close_datarequest_form.html", datarequest=c.datarequest, datasets=c.datasets, errors=c.errors, errors_summary=c.errors_summary %} {% endblock %} -{% block page_header %}{% endblock %} \ No newline at end of file +{% block page_header %}{% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/edit.html b/ckanext/datarequests/templates/datarequests/edit.html index 535920ad..17845e41 100644 --- a/ckanext/datarequests/templates/datarequests/edit.html +++ b/ckanext/datarequests/templates/datarequests/edit.html @@ -3,8 +3,8 @@ {% block subtitle %}{{ _('Edit Data Request') }}{% endblock %} {% block breadcrumb_content %} -
  • {% link_for _('Data Requests'), controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='index' %}
  • -
  • {% link_for c.original_title|truncate(30), controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='show', id=c.datarequest.get('id') %}
  • +
  • {% link_for _('Data Requests'), named_route='datarequest.index' %}
  • +
  • {% link_for c.original_title|truncate(30), named_route='datarequest.show', id=c.datarequest.get('id') %}
  • {{ _('Edit Data Request') }}
  • {% endblock %} @@ -13,4 +13,4 @@

    {% block pa {% snippet "datarequests/snippets/edit_datarequest_form.html", data=c.datarequest, errors=c.errors, errors_summary=c.errors_summary, offering=c.offering %} {% endblock %} -{% block page_header %}{% endblock %} \ No newline at end of file +{% block page_header %}{% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/index.html b/ckanext/datarequests/templates/datarequests/index.html index a9ccd546..0096f723 100644 --- a/ckanext/datarequests/templates/datarequests/index.html +++ b/ckanext/datarequests/templates/datarequests/index.html @@ -6,7 +6,7 @@ {% block page_primary_action %} {% if h.check_access('create_datarequest') %}
    - {% link_for _('Add Data Request'), controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='new', class_='btn btn-primary', icon=h.get_plus_icon() %} + {% link_for _('Add Data Request'), named_route='datarequest.new', class_='btn btn-primary', icon=h.get_plus_icon() %}
    {% endif %} {% snippet 'snippets/custom_search_form.html', query=c.q, fields=(('organization', c.organization), ('state', c.state)), sorting=c.filters, sorting_selected=c.sort, placeholder=_('Search Data Requests...'), no_bottom_border=true, count=c.datarequest_count, no_title=True %} @@ -21,4 +21,4 @@ {% for facet in c.facet_titles %} {{ h.snippet('snippets/facet_list.html', title=c.facet_titles[facet], name=facet) }} {% endfor %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/new.html b/ckanext/datarequests/templates/datarequests/new.html index 1c371e1a..54de71fd 100644 --- a/ckanext/datarequests/templates/datarequests/new.html +++ b/ckanext/datarequests/templates/datarequests/new.html @@ -3,7 +3,7 @@ {% block subtitle %}{{ _('Create Data Request') }}{% endblock %} {% block breadcrumb_content %} -
  • {% link_for _('Data Requests'), controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='index' %}
  • +
  • {% link_for _('Data Requests'), named_route='datarequest.index' %}
  • {{ _('Create Data Request') }}
  • {% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/show.html b/ckanext/datarequests/templates/datarequests/show.html index 89b11f87..0c3d9db9 100644 --- a/ckanext/datarequests/templates/datarequests/show.html +++ b/ckanext/datarequests/templates/datarequests/show.html @@ -5,27 +5,27 @@ {% set datarequest_id = c.datarequest.get('id') %} {% block breadcrumb_content %} -
  • {% link_for _('Data Requests'), controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='index' %}
  • -
  • {% link_for c.datarequest.get('title'), controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='show', id=datarequest_id %}
  • +
  • {% link_for _('Data Requests'), named_route='datarequest.index' %}
  • +
  • {% link_for c.datarequest.get('title'), named_route='datarequest.show', id=datarequest_id %}
  • {% endblock %} {% block content_action %} {% if h.check_access('update_datarequest', {'id':datarequest_id }) %} - {% link_for _('Manage'), controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='update', id=datarequest_id, class_='btn btn-default', icon='wrench' %} + {% link_for _('Manage'), named_route='datarequest.update', id=datarequest_id, class_='btn btn-default', icon='wrench' %} {% endif %} {% if h.check_access('close_datarequest', {'id':datarequest_id }) and not c.datarequest.closed %} - {% link_for _('Close'), controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='close', id=datarequest_id, class_='btn btn-danger', icon='lock' %} + {% link_for _('Close'), named_route='datarequest.close', id=datarequest_id, class_='btn btn-danger', icon='lock' %} {% endif %} {% endblock %} {% block content_primary_nav %} - {{ h.build_nav_icon('show_datarequest', _('Data Request'), id=datarequest_id) }} + {{ h.build_nav_icon('datarequest.show', _('Data Request'), id=datarequest_id, icon=h.get_question_icon()) }} {% if h.show_comments_tab() %} - {{ h.build_nav_icon('comment_datarequest', _('Comments') + ' ' + h.get_comments_badge(datarequest_id), id=datarequest_id) }} + {{ h.build_nav_icon('datarequest.comment', _('Comments') + ' ' + h.get_comments_badge(datarequest_id), id=datarequest_id, icon='comment') }} {% endif %} {% endblock %} @@ -65,4 +65,4 @@

    {% block pa {% snippet "datarequests/snippets/additional_info.html", datarequest=c.datarequest %} {% endblock %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/snippets/additional_info.html b/ckanext/datarequests/templates/datarequests/snippets/additional_info.html index 1c842d87..2fdf89ed 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/additional_info.html +++ b/ckanext/datarequests/templates/datarequests/snippets/additional_info.html @@ -3,7 +3,7 @@

    {{ _('Additional Info') }}

    {% block package_additional_info %} - + @@ -20,18 +20,43 @@

    {{ _('Additional Info') }}

    {% if datarequest.closed %} - - - + + + + {% if datarequest.approx_publishing_date %} + + + + + {% endif %} {% if datarequest.accepted_dataset %} - {% link_for datarequest.accepted_dataset['title'], controller='package', action='read', id=datarequest.accepted_dataset.get('id') %} - {% else %} - {{ _('None') }} + + + + {% endif %} - - + {% else %} + + + + + {% endif %} {% endif %} {% endblock %}
    {{ _('Creator') }} {{ datarequest.user['display_name'] if datarequest.user else _('None') }}
    {{ h.time_ago_from_timestamp(datarequest.close_time) if datarequest.close_time else _('Not closed yet') }}
    {{ _('Accepted Dataset') }} + {% if h.closing_circumstances_enabled %} +
    {{ _('Close circumstance') }} + {{ datarequest.close_circumstance if datarequest.close_circumstance else _('None') }} +
    {{ _('Approximate publishing date') }} + {{ h.render_datetime(datarequest.approx_publishing_date) }} +
    {{ _('Accepted dataset') }} + {% link_for datarequest.accepted_dataset['title'], named_route='package.read', id=datarequest.accepted_dataset.get('id') %} +
    {{ _('Accepted dataset') }} + {% if datarequest.accepted_dataset %} + {% link_for datarequest.accepted_dataset['title'], named_route='package.read', id=datarequest.accepted_dataset.get('id') %} + {% else %} + {{ _('None') }} + {% endif %} +
    - \ No newline at end of file + diff --git a/ckanext/datarequests/templates/datarequests/snippets/close_datarequest_form.html b/ckanext/datarequests/templates/datarequests/snippets/close_datarequest_form.html index 06cd5f80..f1e97a58 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/close_datarequest_form.html +++ b/ckanext/datarequests/templates/datarequests/snippets/close_datarequest_form.html @@ -7,12 +7,35 @@ + {% block closing_circumstance %} + {% if h.closing_circumstances_enabled %} + {% resource 'datarequest/datarequest_close.js' %} + {% set close_circumstances = h.get_closing_circumstances() %} + {% set selected_close_circumstance = datarequest.get('close_circumstance', '') %} + + +
    + +
    + +
    +
    + {{ form.input('approx_publishing_date', id='field-approx_publishing_date', label=_('Approximate publishing date'), placeholder="yyyy-mm-dd", type='date', error=errors.approx_publishing_date) }} + {% endif %} + {% endblock %} + {% block package_basic_fields_tags %}
    + {% endblock %} {% block form_actions %}
    {% block delete_button %} {% if h.check_access('delete_datarequest', {'id': data.get('id', '')}) and not data.state == 'deleted' %} {% set locale = h.dump_json({'content': _('Are you sure you want to delete this data request?')}) %} - {% block delete_button_text %}{{ _('Delete') }}{% endblock %} + {% block delete_button_text %}{{ _('Delete') }}{% endblock %} {% endif %} {% endblock %}
    {% endblock %} - \ No newline at end of file + diff --git a/ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html b/ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html index 773b28d7..7cb6c33f 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html +++ b/ckanext/datarequests/templates/datarequests/snippets/datarequest_item.html @@ -16,18 +16,18 @@

    {% trans %}Open{% endtrans %} {% endif %} - {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='show', id=datarequest.get('id', ''))) }} + {{ h.link_to(h.truncate(title, truncate_title), h.url_for('datarequest.show', id=datarequest.get('id', ''))) }}

    {% if description %}
    {{ description }}
    {% endif %}
    {% if h.show_comments_tab() %} - {{ h.get_comments_number(datarequest.get('id', '')) }} + {{ h.get_comments_number(datarequest.get('id', '')) }} {% endif %}
    {{ h.time_ago_from_timestamp(datarequest.open_time) }}
    {% endblock %} - \ No newline at end of file + diff --git a/ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html b/ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html index 2c14adfb..86c37fbb 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html +++ b/ckanext/datarequests/templates/datarequests/snippets/datarequest_list.html @@ -13,11 +13,11 @@

    {{ _('No Data Requests found with the given criteria') }}. {% if h.check_access('create_datarequest') %} - {% link_for _('How about creating one?'), controller='ckanext.datarequests.controllers.ui_controller:DataRequestsUI', action='new' %} + {% link_for _('How about creating one?'), named_route='datarequest.new' %} {% endif %}

    {% endif %} {% endblock %} {% block page_pagination %} {{ page.pager(q=q) }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/ckanext/datarequests/templates/datarequests/snippets/followers.html b/ckanext/datarequests/templates/datarequests/snippets/followers.html index d21a3e65..02e16fa0 100644 --- a/ckanext/datarequests/templates/datarequests/snippets/followers.html +++ b/ckanext/datarequests/templates/datarequests/snippets/followers.html @@ -17,12 +17,12 @@

    {{ datarequest.get('title') }}