diff --git a/commands/backups.sh b/commands/backups.sh index fe491e1d8..bcc83de72 100755 --- a/commands/backups.sh +++ b/commands/backups.sh @@ -3,12 +3,13 @@ script_dir="$(cd "$(dirname "$0")" && pwd)" # Parse arguments using common.sh function -source ${script_dir}/common.sh +# shellcheck source-path=SCRIPTDIR +source "${script_dir}/common.sh" parse_environment_args "$@" init_environment # Parse backup command from remaining arguments -if [ ${#remaining_args[@]} -eq 0 ]; then +if [[ ${#remaining_args[@]} -eq 0 ]]; then echo "Error: Please provide a backup command." echo "Usage: $0 [--local|--production] [backup_name]" exit 1 @@ -16,12 +17,16 @@ fi BACKUP_CMD="${remaining_args[0]}" -if [[ $BACKUP_CMD = "create" ]]; then - cmd="backup" -elif [[ $BACKUP_CMD = "list" ]]; then - cmd="backups" -elif [[ $BACKUP_CMD = "restore" ]]; then - if [ ${#remaining_args[@]} -lt 2 ]; then +# Build the postgres command as an array so subcommand and backup name +# reach the container as separate argv entries (a quoted single string +# would arrive as one argument, which the postgres entrypoint can't exec). +POSTGRES_CMD=() +if [[ "$BACKUP_CMD" == "create" ]]; then + POSTGRES_CMD=(backup) +elif [[ "$BACKUP_CMD" == "list" ]]; then + POSTGRES_CMD=(backups) +elif [[ "$BACKUP_CMD" == "restore" ]]; then + if [[ ${#remaining_args[@]} -lt 2 ]]; then echo "Error: Please provide backup name for restore." echo "Usage: $0 [--local|--production] restore " exit 1 @@ -30,11 +35,11 @@ elif [[ $BACKUP_CMD = "restore" ]]; then $COMPOSE_BASE_CMD stop django echo "Ensuring postgres service is running for restore..." $COMPOSE_BASE_CMD up -d postgres - cmd="restore ${remaining_args[1]}" + POSTGRES_CMD=(restore "${remaining_args[1]}") else echo "Invalid command $BACKUP_CMD" echo "Usage: $0 [--local|--production] [backup_name]" exit 1 fi -$COMPOSE_BASE_CMD run --rm -it postgres $cmd +$COMPOSE_BASE_CMD run --rm postgres "${POSTGRES_CMD[@]}" diff --git a/commands/common.sh b/commands/common.sh index dd97fcf25..e96d62cdc 100755 --- a/commands/common.sh +++ b/commands/common.sh @@ -2,7 +2,7 @@ # Function to show usage information show_usage() { - local script_name="${1:-$0}" + local script_name="$0" echo "Usage: $script_name [OPTIONS] [COMMAND_ARGS...]" echo "" echo "Environment Options:" @@ -12,6 +12,7 @@ show_usage() { echo " --help, -h Show this help message" echo "" echo "If no environment is specified, auto-detection will be used based on available .envs directories." + return 0 } # Function to parse environment arguments and return remaining arguments @@ -32,6 +33,11 @@ parse_environment_args() { shift ;; --env) + if [[ -z "${2:-}" ]]; then + echo "Error: --env requires a value." >&2 + show_usage >&2 + exit 1 + fi temp_env="$2" shift 2 ;; @@ -53,16 +59,15 @@ parse_environment_args() { # Function to initialize the environment after parsing init_environment() { - # Skip if already initialized - if [ -n "$COMPOSE_BASE_CMD" ]; then + if [[ -n "${COMPOSE_BASE_CMD:-}" ]]; then return 0 fi # Auto-detect environment if not specified - if [ -z "$CODA_ENV" ]; then - if [ -d "$PWD/.envs/.local" ]; then + if [[ -z "$CODA_ENV" ]]; then + if [[ -d "$PWD/.envs/.local" ]]; then CODA_ENV="local" - elif [ -d "$PWD/.envs/.production" ]; then + elif [[ -d "$PWD/.envs/.production" ]]; then CODA_ENV="production" else echo "Error: Cannot determine environment. Please specify --local or --production, or ensure environment files exist." @@ -73,7 +78,7 @@ init_environment() { fi # Validate environment - if [ "$CODA_ENV" != "local" ] && [ "$CODA_ENV" != "production" ]; then + if [[ "$CODA_ENV" != "local" && "$CODA_ENV" != "production" ]]; then echo "Error: Environment must be 'local' or 'production'. Current value: $CODA_ENV" echo "" show_usage @@ -81,23 +86,22 @@ init_environment() { fi # Set up compose command based on environment - if [ "$CODA_ENV" = "local" ]; then + if [[ "$CODA_ENV" == "local" ]]; then export COMPOSE_FILE="compose.local.yml" export ENV_DIR="$PWD/.envs/.local" export COMPOSE_BASE_CMD="docker compose -f $COMPOSE_FILE --env-file $ENV_DIR/django.env --env-file $ENV_DIR/postgres.env" - export POSTGRES_DATA_VOLUME="$(basename $PWD)_coda_local_postgres_data" else export COMPOSE_FILE="compose.production.yml" export ENV_DIR="$PWD/.envs/.production" export COMPOSE_BASE_CMD="docker compose -f $COMPOSE_FILE --env-file $ENV_DIR/coda.env --env-file $ENV_DIR/postgres.env" - export POSTGRES_DATA_VOLUME="$(basename $PWD)_production_postgres_data" fi echo "Using environment: $CODA_ENV (compose file: $COMPOSE_FILE)" } -# Main execution when sourced with arguments (for simple scripts) -if [ $# -gt 0 ]; then +# Standalone execution: resolve and report the environment. +# When sourced (BASH_SOURCE != $0) this file only defines functions. +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then parse_environment_args "$@" init_environment fi diff --git a/commands/create-superuser.sh b/commands/create-superuser.sh index 29661f4d7..7911ceef6 100755 --- a/commands/create-superuser.sh +++ b/commands/create-superuser.sh @@ -1,6 +1,9 @@ #!/bin/bash script_dir="$(cd "$(dirname "$0")" && pwd)" -source ${script_dir}/common.sh "$@" +# shellcheck source-path=SCRIPTDIR +source "${script_dir}/common.sh" +parse_environment_args "$@" +init_environment $COMPOSE_BASE_CMD exec django pdm run manage.py createsuperuser diff --git a/commands/fix-collation.sh b/commands/fix-collation.sh index 06ccc37b8..76e2cf92a 100755 --- a/commands/fix-collation.sh +++ b/commands/fix-collation.sh @@ -3,7 +3,8 @@ script_dir="$(cd "$(dirname "$0")" && pwd)" # Parse arguments using common.sh function -source ${script_dir}/common.sh +# shellcheck source-path=SCRIPTDIR +source "${script_dir}/common.sh" parse_environment_args "$@" init_environment diff --git a/commands/import.sh b/commands/import.sh new file mode 100755 index 000000000..2ccda8446 --- /dev/null +++ b/commands/import.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Import a local data file through a Django management command. +# +# Usage: import.sh [--local|--production] + +script_dir="$(cd "$(dirname "$0")" && pwd)" + +# shellcheck source-path=SCRIPTDIR +source "${script_dir}/common.sh" +parse_environment_args "$@" +init_environment + +usage() { + echo "Usage: $0 [--local|--production] " >&2 + return 0 +} + +if [[ ${#remaining_args[@]} -ne 2 ]]; then + usage + exit 1 +fi + +# The first argument is passed to manage.py verbatim (e.g. +# import_invoices, import_fundingrequests) — new import commands need no +# change here. The regex only rejects paths/flags that can't be commands. +MANAGE_CMD="${remaining_args[0]}" +if ! [[ "$MANAGE_CMD" =~ ^[a-z][a-z0-9_]*$ ]]; then + echo "Error: first argument must be a manage.py command name, e.g. import_fundingrequests." >&2 + usage + exit 1 +fi + +if [[ ! -f "${remaining_args[1]}" ]]; then + echo "Error: file not found: ${remaining_args[1]}" >&2 + exit 1 +fi +MOUNT_DIR="$(cd "$(dirname "${remaining_args[1]}")" && pwd)" || exit 1 +FILE_NAME="$(basename "${remaining_args[1]}")" + +$COMPOSE_BASE_CMD run --rm -v "$MOUNT_DIR:/imports" django pdm run manage.py "$MANAGE_CMD" "/imports/$FILE_NAME" diff --git a/commands/import_invoices.sh b/commands/import_invoices.sh deleted file mode 100755 index 98daa4c89..000000000 --- a/commands/import_invoices.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -script_dir="$(cd "$(dirname "$0")" && pwd)" - -# Parse arguments using common.sh function -source ${script_dir}/common.sh -parse_environment_args "$@" -init_environment - -# Check if file argument was provided -if [ ${#remaining_args[@]} -eq 0 ]; then - echo "Error: Please provide a file path to import." - echo "Usage: $0 [--local|--production] " - exit 1 -fi - -FILE_PATH="${remaining_args[0]}" -MOUNT_DIR=$(dirname "$FILE_PATH") -FILE_NAME=$(basename "$FILE_PATH") - -$COMPOSE_BASE_CMD run --rm -v "$MOUNT_DIR:/imports" django pdm run manage.py import_invoices "/imports/$FILE_NAME" diff --git a/commands/import_requests.sh b/commands/import_requests.sh deleted file mode 100755 index 8994365a6..000000000 --- a/commands/import_requests.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -script_dir="$(cd "$(dirname "$0")" && pwd)" - -# Parse arguments using common.sh function -source ${script_dir}/common.sh -parse_environment_args "$@" -init_environment - -# Check if file argument was provided -if [ ${#remaining_args[@]} -eq 0 ]; then - echo "Error: Please provide a file path to import." - echo "Usage: $0 [--local|--production] " - exit 1 -fi - -FILE_PATH="${remaining_args[0]}" -MOUNT_DIR=$(dirname "$FILE_PATH") -FILE_NAME=$(basename "$FILE_PATH") - -$COMPOSE_BASE_CMD run --rm -v "$MOUNT_DIR:/imports" django pdm run manage.py import_fundingrequests "/imports/$FILE_NAME" diff --git a/commands/start-coda.sh b/commands/start-coda.sh index bf27a9300..f4ea5af47 100755 --- a/commands/start-coda.sh +++ b/commands/start-coda.sh @@ -1,15 +1,29 @@ #!/bin/bash script_dir="$(cd "$(dirname "$0")" && pwd)" -source ${script_dir}/common.sh "$@" +# shellcheck source-path=SCRIPTDIR +source "${script_dir}/common.sh" +parse_environment_args "$@" +init_environment + +# echo owner/repo for GitHub URLs of any scheme (scp-style, ssh://, https://); +# nonzero for anything that doesn't parse, so _get_repo falls through. +_gh_repo() { + local remote_url="$1" + local repo_slug + repo_slug=$(printf '%s' "$remote_url" | sed 's/.*github.com[:\/]//;s/\.git$//') + [[ "$repo_slug" == */* && "$repo_slug" != *:* ]] || return 1 + echo "$repo_slug" +} _get_repo() { - local remote - remote=$(git rev-parse --abbrev-ref @{upstream} 2>/dev/null | cut -d/ -f1) + local remote url + remote=$(git rev-parse --abbrev-ref "@{upstream}" 2>/dev/null | cut -d/ -f1) if [[ -n "$remote" ]]; then - git remote get-url "$remote" 2>/dev/null | sed 's/.*github.com[:\/]//' | sed 's/\.git$//' && return + url=$(git remote get-url "$remote" 2>/dev/null) && _gh_repo "$url" && return fi - git remote get-url origin 2>/dev/null | sed 's/.*github.com[:\/]//' | sed 's/\.git$//' || echo "coda-oa/coda" + url=$(git remote get-url origin 2>/dev/null) && _gh_repo "$url" && return + echo "coda-oa/coda" } start_coda() { diff --git a/commands/stop-coda.sh b/commands/stop-coda.sh index a33bd3cc1..593c54c44 100755 --- a/commands/stop-coda.sh +++ b/commands/stop-coda.sh @@ -1,7 +1,10 @@ #!/bin/bash script_dir="$(cd "$(dirname "$0")" && pwd)" -source ${script_dir}/common.sh "$@" +# shellcheck source-path=SCRIPTDIR +source "${script_dir}/common.sh" +parse_environment_args "$@" +init_environment stop_coda() { $COMPOSE_BASE_CMD down diff --git a/commands/update-coda.sh b/commands/update-coda.sh index 28e3860d6..35d9af1a5 100755 --- a/commands/update-coda.sh +++ b/commands/update-coda.sh @@ -1,20 +1,48 @@ #!/bin/bash - +set -euo pipefail script_dir="$(cd "$(dirname "$0")" && pwd)" # Shared state between step functions STASH_REF="" +STASH_MSG="update-coda pre-update" +ROLLBACK_ON_EXIT=false +PREV_BRANCH="" CODA_STOPPED=false CODA_RESTARTED=false +# True only while the slot STASH_REF still holds this run's tagged entry. +# Guards against another stash landing on top of ours between push and pop. +_stash_is_mine() { + [[ -n "$STASH_REF" ]] && [[ "$(git log -1 --format=%s "$STASH_REF" 2>/dev/null)" == *"$STASH_MSG"* ]] +} + # Cleanup: restart CODA if it was stopped but not restarted cleanup() { + local status=$? + local current_branch + if [[ "$ROLLBACK_ON_EXIT" == true ]]; then + # Only restore the branch if we actually switched: a failure before the + # checkout (e.g. fetch) must not move the user off their current branch. + current_branch="$(git branch --show-current 2>/dev/null || true)" + if [[ -n "${PREV_BRANCH:-}" && "$current_branch" != "$PREV_BRANCH" ]]; then + echo "Rolling back to branch '$PREV_BRANCH' and restoring stashed changes..." >&2 + git checkout "$PREV_BRANCH" 2>/dev/null || true + else + echo "Restoring stashed changes..." >&2 + fi + if [[ -n "$STASH_REF" ]]; then + if _stash_is_mine; then + git stash pop "$STASH_REF" 2>/dev/null || true + else + echo "Stash queue changed; your changes remain under '$STASH_MSG' in 'git stash list'." >&2 + fi + fi + fi if [[ "$CODA_STOPPED" == true && "$CODA_RESTARTED" == false ]]; then echo "Warning: CODA was stopped but the update failed. Restarting..." >&2 - ${script_dir}/start-coda.sh --${CODA_ENV} || true + "${script_dir}/start-coda.sh" --"$CODA_ENV" || true fi - - return 0 + return "$status" } trap cleanup EXIT @@ -50,7 +78,7 @@ parse_update_args() { local arg="$1" case "$arg" in --branch) - local branch_value="$2" + local branch_value="${2:-}" if [[ -z "$branch_value" || "$branch_value" == --* ]]; then echo "Error: --branch requires a value" >&2 exit 1 @@ -83,8 +111,11 @@ parse_update_args() { # Parse arguments before sourcing common.sh parse_update_args "$@" -# Source common.sh for environment setup -source "${script_dir}/common.sh" "$@" +# Source common.sh for environment setup (pure library: define only) +# shellcheck source-path=SCRIPTDIR +source "${script_dir}/common.sh" +parse_environment_args "$@" +init_environment has_uncommitted_changes() { if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then @@ -124,8 +155,7 @@ preflight_checks() { step_create_backup() { if [[ "$CREATE_BACKUP" == true ]]; then echo "Step 1/5: Creating backup..." - ${script_dir}/backups.sh --${CODA_ENV} create - if [[ $? -ne 0 ]]; then + if ! "${script_dir}/backups.sh" --"$CODA_ENV" create; then echo "Error: Backup failed. Aborting update." >&2 return 1 fi @@ -134,68 +164,75 @@ step_create_backup() { echo "Step 1/5: Skipping backup (use --backup to create one)" echo "" fi - return 0 } step_stop_coda() { echo "Step 2/5: Stopping CODA..." - ${script_dir}/stop-coda.sh --${CODA_ENV} - if [[ $? -ne 0 ]]; then + if ! "${script_dir}/stop-coda.sh" --"$CODA_ENV"; then echo "Error: Failed to stop CODA. Aborting update." >&2 return 1 fi CODA_STOPPED=true echo "" - return 0 } step_fetch_and_switch() { echo "Step 3/5: Fetching and switching to branch '$BRANCH'..." + # From here on the repo is in a half-updated state (stashed changes, about + # to switch branches): a failure at ANY later step must restore it, so the + # EXIT trap now rolls back branch and stash. Cleared only on success. + ROLLBACK_ON_EXIT=true + PREV_BRANCH="$(git branch --show-current 2>/dev/null || true)" if has_uncommitted_changes; then echo "Stashing uncommitted changes..." - git stash push --include-untracked + git stash push --include-untracked -m "$STASH_MSG" STASH_REF="stash@{0}" fi - git fetch origin "$BRANCH" - if ! git checkout "$BRANCH" 2>/dev/null; then + if ! git fetch origin "$BRANCH"; then + echo "Error: Fetching '$BRANCH' from origin failed. Aborting update." >&2 + return 1 + fi + if git show-ref --verify --quiet "refs/heads/$BRANCH"; then + git checkout "$BRANCH" + else echo "Branch '$BRANCH' not found locally, creating it from remote..." git checkout -b "$BRANCH" "origin/$BRANCH" fi echo "" - return 0 } step_pull_and_restore() { echo "Step 4/5: Pulling latest changes and restoring stashed changes..." - git pull origin "$BRANCH" - if [[ $? -ne 0 ]]; then - echo "Error: Git pull failed. Restoring original branch..." >&2 - git checkout - 2>/dev/null || true - if [[ -n "${STASH_REF:-}" ]]; then - git stash pop "$STASH_REF" 2>/dev/null || true - fi + if ! git pull origin "$BRANCH"; then + echo "Error: Git pull failed. Original branch and local changes will be restored." >&2 return 1 fi # Restore stashed changes if we created one in this run - if [[ -n "${STASH_REF:-}" ]] && ! git stash pop "$STASH_REF" 2>&1; then - echo "Warning: Stash restore had conflicts." >&2 - echo "Your local changes may need manual conflict resolution." >&2 + if [[ -n "$STASH_REF" ]]; then + if _stash_is_mine; then + if ! git stash pop "$STASH_REF" 2>&1; then + echo "Warning: Stash restore had conflicts." >&2 + echo "Your local changes may need manual conflict resolution." >&2 + else + STASH_REF="" + fi + else + echo "Warning: The stash queue changed during the update." >&2 + echo "Your changes are safe under '$STASH_MSG' — see 'git stash list'." >&2 + fi fi echo "" - return 0 } step_start_coda() { echo "Step 5/5: Starting CODA (this will rebuild containers and run migrations)..." - ${script_dir}/start-coda.sh --${CODA_ENV} - if [[ $? -ne 0 ]]; then + if ! "${script_dir}/start-coda.sh" --"$CODA_ENV"; then echo "Error: Failed to start CODA." >&2 return 1 fi CODA_RESTARTED=true echo "" - return 0 } update_coda() { @@ -215,11 +252,12 @@ update_coda() { echo "Backup: $([[ "$CREATE_BACKUP" == true ]] && echo "Yes" || echo "No")" echo "" - step_create_backup || exit 1 - step_stop_coda || exit 1 - step_fetch_and_switch || exit 1 - step_pull_and_restore || exit 1 - step_start_coda || exit 1 + step_create_backup + step_stop_coda + step_fetch_and_switch + step_pull_and_restore + step_start_coda + ROLLBACK_ON_EXIT=false echo "Update completed successfully!" echo "" diff --git a/commands/upgrade-postgres.sh b/commands/upgrade-postgres.sh index a003395fc..523eb6045 100755 --- a/commands/upgrade-postgres.sh +++ b/commands/upgrade-postgres.sh @@ -3,7 +3,8 @@ script_dir="$(cd "$(dirname "$0")" && pwd)" # Parse arguments using common.sh function, but handle postgres-version specially -source ${script_dir}/common.sh +# shellcheck source-path=SCRIPTDIR +source "${script_dir}/common.sh" postgres_version="" filtered_args=() @@ -12,6 +13,10 @@ filtered_args=() while [[ $# -gt 0 ]]; do case $1 in --postgres-version) + if [[ -z "${2:-}" ]]; then + echo "Error: --postgres-version requires a value." >&2 + exit 1 + fi postgres_version="$2" shift 2 ;; @@ -28,21 +33,36 @@ init_environment # Load postgres environment variables POSTGRES_ENV_FILE="$ENV_DIR/postgres.env" -source $POSTGRES_ENV_FILE +# shellcheck source=/dev/null +. "$POSTGRES_ENV_FILE" # Override POSTGRES_VERSION if provided via command line -if [ -n "$postgres_version" ]; then +if [[ -n "$postgres_version" ]]; then POSTGRES_VERSION="$postgres_version" echo "Overriding PostgreSQL version with command line value: $POSTGRES_VERSION" fi # Validate that POSTGRES_VERSION is set -if [ -z "$POSTGRES_VERSION" ]; then +if [[ -z "$POSTGRES_VERSION" ]]; then echo "Error: PostgreSQL version not specified. Please set POSTGRES_VERSION in $POSTGRES_ENV_FILE or use --postgres-version flag." echo "Usage: $0 [--local|--production] [--postgres-version VERSION]" exit 1 fi +# Resolve the real compose-managed volume: key from the compose file itself, +# actual name from Docker's labels (immune to project/checkout renames). +mapfile -t VOLUME_KEY_ARRAY < <($COMPOSE_BASE_CMD config --volumes | grep -E '_postgres_data$') +if [[ ${#VOLUME_KEY_ARRAY[@]} -ne 1 ]]; then + echo "Error: expected exactly one *_postgres_data volume in $COMPOSE_FILE, got ${#VOLUME_KEY_ARRAY[@]}." >&2 + exit 1 +fi +VOLUME_KEYS="${VOLUME_KEY_ARRAY[0]}" +POSTGRES_DATA_VOLUME="$(docker volume ls -q --filter "label=com.docker.compose.volume=${VOLUME_KEYS}" | head -1)" +if [[ -z "$POSTGRES_DATA_VOLUME" ]]; then + echo "Error: no compose-managed volume '${VOLUME_KEYS}' found. Aborting before any destructive step." >&2 + exit 1 +fi + echo "########################################################" echo "# Upgrading PostgreSQL Version" echo "# Environment: ${CODA_ENV}" @@ -54,33 +74,35 @@ echo "" echo "# Creating backup of PostgreSQL data volume ${POSTGRES_DATA_VOLUME}..." echo "" -$PWD/commands/backups.sh create --${CODA_ENV} +if ! "$PWD"/commands/backups.sh create --"$CODA_ENV"; then + echo "Error: Backup failed. Aborting before the destructive upgrade." >&2 + exit 1 +fi echo "Shutting down CODA before PostgreSQL upgrade" -source ${script_dir}/stop-coda.sh -stop_coda +$COMPOSE_BASE_CMD down echo "Using pgautoupgrade image: pgautoupgrade/pgautoupgrade:${POSTGRES_VERSION}" -docker run --rm -e PGAUTO_ONESHOT=yes --env-file $POSTGRES_ENV_FILE -v ${POSTGRES_DATA_VOLUME}:/var/lib/postgresql/data pgautoupgrade/pgautoupgrade:${POSTGRES_VERSION} +docker run --rm -e PGAUTO_ONESHOT=yes --env-file "$POSTGRES_ENV_FILE" -v "${POSTGRES_DATA_VOLUME}":/var/lib/postgresql/data "pgautoupgrade/pgautoupgrade:${POSTGRES_VERSION}" echo "" echo "# PostgreSQL upgrade completed. Rebuilding container with new version..." echo "" # Rebuild postgres image with new version -docker compose -f $COMPOSE_FILE --env-file $ENV_DIR/coda.env --env-file $POSTGRES_ENV_FILE build --build-arg POSTGRES_VERSION=${POSTGRES_VERSION} postgres +$COMPOSE_BASE_CMD build --build-arg POSTGRES_VERSION="${POSTGRES_VERSION}" postgres echo "" echo "# Starting PostgreSQL ${POSTGRES_VERSION} and checking collation version..." echo "" # Start with the new version -docker compose -f $COMPOSE_FILE --env-file $ENV_DIR/coda.env --env-file $POSTGRES_ENV_FILE up -d postgres +$COMPOSE_BASE_CMD up -d postgres # Wait for postgres to be ready (with retries) echo "Waiting for PostgreSQL to be ready..." for i in {1..30}; do - if docker compose -f $COMPOSE_FILE --env-file $ENV_DIR/coda.env --env-file $POSTGRES_ENV_FILE exec -T postgres pg_isready -U django > /dev/null 2>&1; then + if $COMPOSE_BASE_CMD exec -T postgres pg_isready -U "${POSTGRES_USER:?POSTGRES_USER missing in $POSTGRES_ENV_FILE}" > /dev/null 2>&1; then echo "PostgreSQL is ready!" break fi @@ -89,4 +111,4 @@ for i in {1..30}; do done # Run collation fix -docker compose -f $COMPOSE_FILE --env-file $ENV_DIR/coda.env --env-file $POSTGRES_ENV_FILE run --rm postgres fix-collation +$COMPOSE_BASE_CMD run --rm postgres fix-collation diff --git a/docs/users/features/fundingrequests.md b/docs/users/features/fundingrequests.md index e8b4c4d16..3063565f7 100644 --- a/docs/users/features/fundingrequests.md +++ b/docs/users/features/fundingrequests.md @@ -1,6 +1,6 @@ # Funding Requests -Funding Requests are the core of CODA's workflow, allowing you to process publication funding and enabling your institution to review and approve those requests systematically. You can access the funding requests over the **Request Center** navigation item. +Funding Requests are the core of CODA's workflow, allowing you to process publication funding and enabling your institution to review and approve those requests systematically. You can access the funding requests over the **Request Center** navigation item. ## Overview @@ -73,14 +73,13 @@ Search and select a journal from CODA's database of over 26,000 entries: For monograph requests, you'll select a publisher instead of a journal. -You can also add a contract to the funding request. +You can also add a contract to the funding request. ![](/_static/img/fundingrequests_journal_step.png) - ### Step 2: Publication Details -In the second step author information, publication meta data and links (e.g. DOI, ISBN, PMC and more) can be added to the funding request. +In the second step author information, publication meta data and links (e.g. DOI, ISBN, PMC and more) can be added to the funding request. **Authors:** @@ -92,26 +91,27 @@ Collect information about the authors submitting the request: - **Affiliation**: Institutional affiliation - **Role**: The submitter's role in the publication (e.g., Corresponding Author or Submitter) -You can add one row per author and give the different roles. +You can add one row per author and give the different roles. ```{admonition} Tip -You can copy author information and paste it into the text area and let CODA parse the copied text to structured author data. This feature keeps author information that is not relevant for your process, for instance when many authors contributed, but only the corresponding author is interesting for you. +You can copy author information and paste it into the text area and let CODA parse the copied text to structured author data. This feature keeps author information that is not relevant for your process, for instance when many authors contributed, but only the corresponding author is interesting for you. ``` ![](/_static/img/fundingrequests_authors.png) - **Publication Metadata:** Record comprehensive metadata about the publication: **Basic Information:** + - **Title**: Publication title - **License**: Open access license (CC BY, CC BY-SA, etc.) - **Publication Type**: Default is based on [COAR Resource Types Vocabulary 3.1](https://vocabularies.coar-repositories.org/resource_types/3.1/); can be edited in the [vocabularies](vocabularies.md). - **Subject Area**: Default is based on [DFG Subject Classification](https://www.dfg.de/resource/blob/331950/85717c3edb9ea8bd453d5110849865d3/fachsystematik-2024-2028-en-data.pdf); can be edited in the [vocabularies](vocabularies.md). **Publication Status:** + - Unknown - Submitted - Accepted @@ -119,13 +119,12 @@ Record comprehensive metadata about the publication: - Published **Dates:** + - Online publication date - print publication date - **Additional References/Links:** -You ca add different kinds of identifiers and links related to the publication. For instance DOI, PMC, ISBN, Handle etc. The UI will validate most of the available link types to ensure correct data. - +You ca add different kinds of identifiers and links related to the publication. For instance DOI, PMC, ISBN, Handle etc. The UI will validate most of the available link types to ensure correct data. ```{admonition} Configurable Vocabularies The Publication Type and Subject Area fields are based on predefined vocabularies that can be configured by your institution in the [vocabularies](vocabularies.md) section. They are then set in the [Preferences](preferences.md). By default, CODA uses COAR Resource Types 3.1 and DFG Subject Classification. @@ -138,10 +137,11 @@ The Publication Type and Subject Area fields are based on predefined vocabularie In this step you can collect financial information: **Estimated Costs:** + - **Amount**: Estimated publication costs - **Currency**: Select the currency - **Payment Method**: Direct, Reimbursement, or Unknown -- **External cost splitting**: Tick this box to show that you shared costs with an external partner institute. This information is relevant for an [openCost report](reporting.md). +- **External cost splitting**: Tick this box to show that you shared costs with an external partner institute. This information is relevant for an [openCost report](reporting.md). **External Research Funding:** @@ -160,10 +160,9 @@ As the final costs of a publication are not always known when a funding request ![](/_static/img/fundingrequests_funding.png) - ### Step 4: Additional contact information -In this final step you can provide additional contact information regarding the funding request besides the authors information. You can also add notes and remarks. +In this final step you can provide additional contact information regarding the funding request besides the authors information. You can also add notes and remarks. ![](/_static/img/fundingrequests_contact.png) @@ -182,12 +181,11 @@ CODA runs automated checks against institutional policies to help guide your dec - **DOAJ Check**: Verifies if the journal is listed in the Directory of Open Access Journals - **Success** (green): Journal found in DOAJ with link to entry - **Failed** (red): Journal not listed in DOAJ - + - **Blocklist Check**: Checks if the journal or publisher is on your institution's [blocklist](blocklist.md) - **Success** (green): Journal and publisher are not blocked - **Warning** (yellow): Journal is blocked but needs review (6+ months old) - **Failed** (red): Journal or publisher is actively blocked - Check results provide immediate feedback on potential policy violations, but final approval decisions remain with reviewers. They are displayed in the detail's page right column. @@ -200,11 +198,13 @@ From the funding request detail page, click **Submit Review** to open the dedica It allows you to: **Set Decided Funding Amount:** + - Enter the approved funding amount - Select the currency - This can differ from the estimated cost if negotiation occurred **Add Reviewer Remarks:** + - Record notes about the review decision - Document special circumstances or exceptions @@ -230,6 +230,7 @@ Labels are custom tags you can attach to funding requests for: - Reporting and filtering (e.g., "Special Funds", "Research Initiative") Each label has: + - **Name**: Descriptive text - **Color**: You can select the color by using a color picker. @@ -254,7 +255,6 @@ From the funding request detail page: Labels are visible on both the detail page and the overview list. - ## Importing Funding Requests CODA supports bulk importing of funding requests from external systems using JSON files. This is useful for: @@ -351,7 +351,7 @@ Funding requests are imported using JSON files that follow a specific schema. Th } }, "estimated_cost": { - "amount": 2000.00, + "amount": 2000.0, "currency": "EUR", "payment_method": "direct" }, @@ -365,7 +365,7 @@ Funding requests are imported using JSON files that follow a specific schema. Th "review": { "result": "approved", "decided_funding": { - "amount": 1800.00, + "amount": 1800.0, "currency": "EUR" }, "remarks": "Approved with negotiated discount" @@ -385,12 +385,14 @@ Funding requests are imported using JSON files that follow a specific schema. Th The complete JSON schema is available for download [here](/_static/downloads/fundingrequest_import_schema.json). Key fields include: **Request Level:** + - `request_date` (required): Date in YYYY-MM-DD format - `legacy_request_id`: Identifier from your old system - `request_remarks`: Notes or comments about the request - `labels`: Array of label names (labels are auto-created if they don't exist) **Publication:** + - `title` (required): Publication title - `kind` (required): Either "article" or "monograph" - `license`: CC-BY, CC-BY-SA, CC-BY-NC, CC-BY-NC-SA, CC-BY-NC-ND, CC-BY-ND, CC0, Unknown, Proprietary, None @@ -406,16 +408,19 @@ The complete JSON schema is available for download [here](/_static/downloads/fun - `subject_area`: DFG classification or custom vocabulary **Estimated Cost:** + - `amount`: Numeric value (can be string or number) - `currency`: Three-letter currency code (EUR, USD, GBP, etc.) - `payment_method`: "direct", "reimbursement", or "unknown" **Research Funding:** + - `organization_name`: Funder name (auto-creates if doesn't exist) - `project_id`: Grant or project identifier - `project_name`: Full project name **Review:** + - `result`: "open", "approved", "rejected", "waived", or "closed" - `decided_funding`: Amount and currency - `remarks`: Reviewer notes @@ -427,7 +432,6 @@ The complete JSON schema is available for download [here](/_static/downloads/fun 3. Upload your JSON file 4. Click **Save** to start the import - ### Import Behavior **Auto-Creation of Related Entities:** @@ -444,6 +448,7 @@ CODA automatically creates missing related entities during import: **Review Status:** Imported requests can have any review status: + - Import historical **approved** or **rejected** requests with their decisions - Import **open** requests for ongoing review - Include decided funding amounts and reviewer remarks @@ -451,6 +456,7 @@ Imported requests can have any review status: **Validation:** The import validates all data against the schema: + - Required fields must be present - Dates must be in YYYY-MM-DD format - Currencies must be valid three-letter codes @@ -466,11 +472,13 @@ Imported requests bypass automated checks (DOAJ, Blocklist). This allows importi After import, you'll see: **Success Message:** + ``` Successfully imported 5 funding request(s). ``` **Partial Success with Errors:** + ``` Successfully imported 3 funding request(s). 2 request(s) failed to import. See details below. @@ -486,17 +494,20 @@ OLD-ID-456: Contract with name 'Unknown Contract' and year 2025 not found ``` Each error references either: + - The `legacy_request_id` if provided - The publication `title` if no legacy ID exists - ### Command-Line Import For system administrators, CODA provides a command-line import tool: ```bash # Using the shell script -./commands/import_requests.sh --local /path/to/requests.json +./commands/import.sh --production import_fundingrequests /path/to/requests.json + +# Or for local environments +./commands/import.sh --local import_fundingrequests /path/to/requests.json # Or directly via Django management command pdm run manage.py import_fundingrequests /path/to/requests.json @@ -505,4 +516,3 @@ pdm run manage.py import_fundingrequests /path/to/requests.json This is useful for large batch imports that might timeout in the browser The command-line import provides the same validation and error reporting as the web interface. - diff --git a/docs/users/features/invoices.md b/docs/users/features/invoices.md index 835437e95..a25ce5403 100644 --- a/docs/users/features/invoices.md +++ b/docs/users/features/invoices.md @@ -470,10 +470,10 @@ For system administrators, CODA provides a command-line import tool: ```bash # Using the shell script -./commands/import_invoices.sh --production /path/to/invoices.json +./commands/import.sh --production import_invoices /path/to/invoices.json # Or for local environment -./commands/import_invoices.sh --local /path/to/invoices.json +./commands/import.sh --local import_invoices /path/to/invoices.json ``` This is useful for: