diff --git a/.github/workflows/build-push-ecr.yml b/.github/workflows/build-push-ecr.yml new file mode 100644 index 00000000000..5cfd6e4a621 --- /dev/null +++ b/.github/workflows/build-push-ecr.yml @@ -0,0 +1,109 @@ +name: Build & Push AMP image to ECR + +on: + push: + branches: + - "**" # all branches + workflow_dispatch: # allow manual trigger + +jobs: + build-push: + runs-on: ubuntu-latest + + steps: + # ── Load SSH key (needed for submodule checkout AND private npm packages) ─ + - name: Start SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.DOCKER_BUILD_SSH_KEY }} + + - name: Configure git to use SSH for submodules + run: | + git config --global url."git@github.com:".insteadOf "https://github.com/" + mkdir -p ~/.ssh + ssh-keyscan -H github.com >> ~/.ssh/known_hosts 2>/dev/null || true + + # ── Checkout (with submodules — Dockerfile COPYs .git dirs from them) ─── + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + ssh-key: ${{ secrets.DOCKER_BUILD_SSH_KEY }} + + # ── Derive the Docker tag from the branch name ───────────────────────── + # Sanitise: replace / with - and strip other unsafe chars (Docker tag rules). + - name: Set image tag + id: tag + run: | + RAW="${{ github.ref_name }}" + TAG="${RAW//\//-}" + TAG="${TAG//[^a-zA-Z0-9._-]/-}" + echo "value=${TAG}" >> "$GITHUB_OUTPUT" + + # ── Configure AWS credentials ───────────────────────────────────────── + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + # ── Log in to Amazon ECR ────────────────────────────────────────────── + - name: Login to Amazon ECR + id: ecr-login + run: | + set +x + PASSWORD=$(aws ecr get-login-password --region us-east-1) + echo "::add-mask::$PASSWORD" + echo "$PASSWORD" | docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY }} + echo "✅ Logged in to ECR" + + # ── Set up Docker Buildx ───────────────────────────────────────────── + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: | + image=moby/buildkit:latest + network=host + + # ── Build & push ────────────────────────────────────────────────────── + # ECR_REGISTRY → e.g. 798366298150.dkr.ecr.us-east-1.amazonaws.com + # Image repo path is hardcoded to amp/webapp (matching deploy.yml convention) + - name: Build and push AMP image + env: + DOCKER_BUILDKIT: 1 + BUILDKIT_PROGRESS: plain + run: | + TAG="${{ steps.tag.outputs.value }}" + IMAGE="${{ secrets.ECR_REGISTRY }}/amp/webapp" + + docker buildx build \ + --progress=plain \ + --ssh default \ + --build-arg SKIP_TESTS=true \ + --build-arg BUILD_SOURCE="${TAG}" \ + --build-arg AMP_BRANCH="${{ github.ref_name }}" \ + --cache-from type=registry,ref="${IMAGE}:${TAG}" \ + --cache-from type=registry,ref="${IMAGE}:latest" \ + --cache-to type=inline \ + --build-arg BUILDKIT_INLINE_CACHE=1 \ + -t "${IMAGE}:${TAG}" \ + -t "${IMAGE}:latest" \ + --push \ + amp + + # ── Output ──────────────────────────────────────────────────────────── + - name: Print image reference + run: | + echo "Image pushed:" + echo " ${{ secrets.ECR_REGISTRY }}/amp/webapp:${{ steps.tag.outputs.value }}" + echo "" + echo "Set in deployment .env:" + echo " AMP_IMAGE=${{ secrets.ECR_REGISTRY }}/amp/webapp" + echo " AMP_TAG=${{ steps.tag.outputs.value }}" + + # ── Logout ─────────────────────────────────────────────────────────── + - name: Logout from ECR + if: always() + run: docker logout ${{ secrets.ECR_REGISTRY }} || true diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 264ffdd01d3..b00448a1a73 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,6 +17,8 @@ on: options: - bfaso - boad + - cameroon + - cameroontrubudget - chad - civ - drc diff --git a/amp/.env.example b/amp/.env.example new file mode 100644 index 00000000000..0ead11c592e --- /dev/null +++ b/amp/.env.example @@ -0,0 +1,68 @@ +# AMP + TruBudget Docker environment +# Copy this file to .env in the repo root, fill in values, then run ./setup.sh +# Do NOT commit .env to version control. + +# ── Server ───────────────────────────────────────────────────────────────── +# The private IP of this server +SERVER_IP=192.168.1.100 + +# ── Ports (host-side) ────────────────────────────────────────────────────── +AMP_PORT=8080 +TRUBUDGET_UI_PORT=3000 +TRUBUDGET_API_PORT=8081 + +# ── AMP ──────────────────────────────────────────────────────────────────── +# Docker image produced by the GitHub Actions CI pipeline (build-push-ecr.yml). +# AMP_IMAGE → ECR repository URL (no tag) +# AMP_TAG → branch or release tag built by CI (e.g. develop, main, v1.2.0) +AMP_IMAGE=798366298150.dkr.ecr.us-east-1.amazonaws.com/amp/webapp +AMP_TAG=task/AMP-31085/Update-TruBudget-Branch-with-latest-develolp + +# AWS credentials used by setup.sh to authenticate to ECR before pulling +AWS_ACCESS_KEY_ID=your_access_key_id +AWS_SECRET_ACCESS_KEY=your_secret_access_key +AWS_DEFAULT_REGION=us-east-1 + +AMP_DB_NAME=amp +AMP_DB_USER=amp +AMP_DB_PASSWORD=change_me_amp_db_pass + +# Optional: label shown in the AMP footer next to the version number. +# Replaces the raw CI branch/tag name (e.g. task-AMP-31085-...) with something readable. +# Leave empty to show the branch name as-is. +AMP_BUILD_LABEL=v4.0 + +# Optional: full footer text shown below the version line. +# Replaces whatever is stored in the DB for the footer key. +# Example: "AMP Senegal — Developed by Development Gateway" +# Leave empty to keep the DB value unchanged. +AMP_FOOTER_TEXT=TruBudget Enabled AMP Cameroon Developed by Development Gateway with the support of the European Union, GIZ and UNDP + +# Tomcat JVM heap (e.g. 8g for a 16 GB instance) +AMP_JVM_HEAP=2g + +# SMTP relay for AMP outbound mail (optional) +SMTP_HOST=smtp.example.org +SMTP_FROM=noreply@example.org + +# Docker-internal URL the AMP JVM uses to reach TruBudget API. +TRUBUDGET_BASE_URL=http://host.docker.internal:8081/ +TRUBUDGET_ENABLED=false + +# ── TruBudget (cloned from official repo) ───────────────────────────────── +# Git tag or branch — use a specific tag like v2.10.0 for production stability +TRUBUDGET_TAG=main +# Docker image tag used inside TruBudget's own compose (latest, main, or v2.x.x) +TRUBUDGET_VERSION=latest + +# Organisation name on the blockchain (no spaces) +TRUBUDGET_ORG=AMP_ORG + +# Vault secret used to encrypt private keys — keep this safe +TRUBUDGET_VAULT_SECRET=change_me_vault_secret + +# Root admin password for first-time TruBudget login (username: root) +TRUBUDGET_ROOT_SECRET=change_me_root_secret + +# Blockchain internal RPC password +TRUBUDGET_RPC_PASSWORD=change_me_rpc_password diff --git a/amp/Dockerfile b/amp/Dockerfile index 167999bbd7a..f7eb59d245e 100644 --- a/amp/Dockerfile +++ b/amp/Dockerfile @@ -191,6 +191,14 @@ RUN npm run build \ && rm -rf node_modules FROM maven:3.8.4-jdk-8 as compile-mvn WORKDIR /tmp/amp + +# JDBC credentials passed in from docker-compose build args (see docker-compose.yml) +ARG JDBC_USER=amp +ARG JDBC_PASSWORD=amp +ARG JDBC_DB=amp +ARG JDBC_HOST=db +ARG JDBC_PORT=5432 + # Copy pom.xml first for better Maven caching COPY pom.xml . #COPY amp/pom.xml amp/ @@ -206,24 +214,26 @@ COPY --from=compile-reampv2 /tmp/amp/TEMPLATE/reampv2 TEMPLATE/reampv2 # Download Maven dependencies (this layer will be cached if pom.xml doesn't change) ARG BUILD_SOURCE RUN --mount=type=cache,target=/root/.m2 \ - mvn -B dependency:go-offline -f pom.xml || true + mvn -B org.apache.maven.plugins:maven-dependency-plugin:3.1.2:go-offline -f pom.xml || true # Copy source code after dependencies are downloaded COPY . . ARG SKIP_TESTS=false RUN --mount=type=cache,target=/root/.m2 \ if [ "$SKIP_TESTS" = "true" ]; then \ - mvn -B clean compile war:exploded \ + mvn -B clean compile org.apache.maven.plugins:maven-war-plugin:2.3:exploded \ -DbuildSource=$BUILD_SOURCE \ - -Djdbc.user=amp -Djdbc.password=amp122006 -Djdbc.db=amp -Djdbc.host=db \ - -Djdbc.port=5432 -DdbName=postgresql -Djdbc.driverClassName=org.postgresql.Driver \ + -Djdbc.user=$JDBC_USER -Djdbc.password=$JDBC_PASSWORD \ + -Djdbc.db=$JDBC_DB -Djdbc.host=$JDBC_HOST -Djdbc.port=$JDBC_PORT \ + -DdbName=postgresql -Djdbc.driverClassName=org.postgresql.Driver \ -Dskip.npm -Dskip.installnodenpm -DskipTests \ && mv target/amp exploded \ && rm -rf target; \ else \ - mvn -B test war:exploded \ + mvn -B test org.apache.maven.plugins:maven-war-plugin:2.3:exploded \ -DbuildSource=$BUILD_SOURCE \ - -Djdbc.user=amp -Djdbc.password=amp122006 -Djdbc.db=amp -Djdbc.host=db \ - -Djdbc.port=5432 -DdbName=postgresql -Djdbc.driverClassName=org.postgresql.Driver \ + -Djdbc.user=$JDBC_USER -Djdbc.password=$JDBC_PASSWORD \ + -Djdbc.db=$JDBC_DB -Djdbc.host=$JDBC_HOST -Djdbc.port=$JDBC_PORT \ + -DdbName=postgresql -Djdbc.driverClassName=org.postgresql.Driver \ -Dskip.npm -Dskip.installnodenpm \ && mv target/amp exploded \ && rm -rf target; \ @@ -232,7 +242,7 @@ RUN --mount=type=cache,target=/root/.m2 \ FROM tomcat:8.5.79-jdk8 RUN apt-get update \ - && apt-get install -y apt-transport-https ca-certificates curl gnupg --no-install-recommends \ + && apt-get install -y apt-transport-https ca-certificates curl gnupg postgresql-client --no-install-recommends \ && curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ && echo "deb https://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ && apt-get update \ @@ -256,4 +266,11 @@ LABEL "branch"=$AMP_BRANCH ENV AMP_REGISTRY_PRIVATE_KEY $AMP_REGISTRY_PRIVATE_KEY RUN rm -fr /usr/local/tomcat/webapps/ROOT -COPY --from=compile-mvn /tmp/amp/exploded /usr/local/tomcat/webapps/ROOT/ \ No newline at end of file +COPY --from=compile-mvn /tmp/amp/exploded /usr/local/tomcat/webapps/ROOT/ + +# Entrypoint patches context.xml with runtime JDBC env vars before Tomcat starts, +# so the same image works with any DB password without a rebuild. +COPY docker/entrypoint.sh bin/entrypoint.sh +RUN chmod +x /usr/local/tomcat/bin/entrypoint.sh + +ENTRYPOINT ["/usr/local/tomcat/bin/entrypoint.sh"] \ No newline at end of file diff --git a/amp/docker/entrypoint.sh b/amp/docker/entrypoint.sh new file mode 100644 index 00000000000..6ecc42e4ad9 --- /dev/null +++ b/amp/docker/entrypoint.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# entrypoint.sh — Patch config files with runtime env vars before Tomcat starts. +# +# Patches performed: +# 1. context.xml — JDBC credentials (user, password, URL) from JDBC_* env vars +# 2. site-config.xml — buildSource entity from AMP_BUILD_LABEL env var (optional) +# +# Footer text ("AMP Togo...") is stored in the DB (DG_MESSAGE table). +# It is patched at container startup via psql if AMP_FOOTER_TEXT is set. +# The DB patch is done here via a background wait-and-patch so Tomcat can start +# while we wait for the DB to accept connections. +# +# Expected env vars: +# JDBC_USER, JDBC_PASSWORD, JDBC_DB, JDBC_HOST, JDBC_PORT +# AMP_BUILD_LABEL (optional) — replaces the branch-name build tag in the footer +# AMP_FOOTER_TEXT (optional) — replaces the full footer text in the DB + +set -euo pipefail + +CONTEXT_XML="/usr/local/tomcat/webapps/ROOT/META-INF/context.xml" +SITE_CONFIG="/usr/local/tomcat/webapps/ROOT/TEMPLATE/ampTemplate/site-config.xml" + +# ── 1. Patch context.xml (JDBC credentials) ────────────────────────────────── +if [[ -f "${CONTEXT_XML}" ]]; then + echo "[entrypoint] Patching context.xml with runtime JDBC credentials..." + sed -i \ + -e "s|username=\"[^\"]*\"|username=\"${JDBC_USER:-amp}\"|g" \ + -e "s|password=\"[^\"]*\"|password=\"${JDBC_PASSWORD:-amp}\"|g" \ + "${CONTEXT_XML}" + sed -i \ + -e "s|jdbc:postgresql://[^/]*/[^\"]*|jdbc:postgresql://${JDBC_HOST:-db}:${JDBC_PORT:-5432}/${JDBC_DB:-amp}|g" \ + "${CONTEXT_XML}" + echo "[entrypoint] context.xml patched." +else + echo "[entrypoint] WARNING: ${CONTEXT_XML} not found — skipping JDBC patch." +fi + +# ── 2. Patch site-config.xml (build label) ─────────────────────────────────── +if [[ -n "${AMP_BUILD_LABEL:-}" ]] && [[ -f "${SITE_CONFIG}" ]]; then + echo "[entrypoint] Setting buildSource to: ${AMP_BUILD_LABEL}" + sed -i "s|||" "${SITE_CONFIG}" + echo "[entrypoint] site-config.xml patched." +fi + +# ── 3. Patch footer text in DB (background — waits for DB to be ready) ─────── +if [[ -n "${AMP_FOOTER_TEXT:-}" ]]; then + ( + DB_HOST="${JDBC_HOST:-db}" + DB_PORT="${JDBC_PORT:-5432}" + DB_NAME="${JDBC_DB:-amp}" + DB_USER="${JDBC_USER:-amp}" + PGPASSWORD="${JDBC_PASSWORD:-amp}" + export PGPASSWORD + + echo "[entrypoint] Waiting for DB to accept connections before patching footer text..." + for i in $(seq 1 30); do + if psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" -c "SELECT 1" &>/dev/null; then + echo "[entrypoint] DB ready. Updating footer text in DG_MESSAGE..." + # Update all locales for the footer key. Uses DO ... to upsert cleanly. + psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" <jackson-databind ${jackson.version} + + io.projectreactor.netty + reactor-netty + 1.0.35 + + + org.springframework + spring-webflux + 5.3.28 + com.fasterxml.jackson.jaxrs jackson-jaxrs-json-provider diff --git a/amp/src/main/java/org/dgfoundation/amp/aitranslation/CachedMachineTranslationService.java b/amp/src/main/java/org/dgfoundation/amp/aitranslation/CachedMachineTranslationService.java index 3bd8204e5e9..6e4ad6c2ef3 100644 --- a/amp/src/main/java/org/dgfoundation/amp/aitranslation/CachedMachineTranslationService.java +++ b/amp/src/main/java/org/dgfoundation/amp/aitranslation/CachedMachineTranslationService.java @@ -10,6 +10,9 @@ import org.hibernate.Session; import org.hibernate.criterion.Restrictions; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Root; import java.time.LocalDate; import java.util.ArrayList; import java.util.HashMap; @@ -73,12 +76,14 @@ private Pair, List> loadCachedTranslations( for (int i = 0; i < contents.size(); i += BATCH_SIZE) { List batchContents = contents.subList(i, Math.min(i + BATCH_SIZE, contents.size())); - translations.addAll(PersistenceManager.getSession() - .createCriteria(MachineTranslation.class) - .add(Restrictions.eq("sourceLanguage", srcLang)) - .add(Restrictions.eq("targetLanguage", destLang)) - .add(Restrictions.in("text", batchContents)) - .list()); + Session session = PersistenceManager.getRequestDBSession(); + CriteriaBuilder builder = session.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = builder.createQuery(MachineTranslation.class); + Root root = criteriaQuery.from(MachineTranslation.class); + criteriaQuery.select(root).where(builder.equal(root.get("sourceLanguage"), srcLang),builder.equal(root.get("targetLanguage"), destLang), + builder.equal(root.get("text"), batchContents)); + + translations.addAll(session.createQuery(criteriaQuery).list()); } return translations.stream().map(o -> ((MachineTranslation) o)) @@ -124,10 +129,13 @@ private Predicate upToCodePoints(int charsAllowed) { } private int getCharactersUsedThisMonth() { - MachineTranslationCharactersUsed used = (MachineTranslationCharactersUsed) PersistenceManager.getSession() - .createCriteria(MachineTranslationCharactersUsed.class) - .add(Restrictions.eq("id", 1L)) - .uniqueResult(); + Session session = PersistenceManager.getRequestDBSession(); + CriteriaBuilder builder = session.getCriteriaBuilder(); + CriteriaQuery criteriaQuery = builder.createQuery(MachineTranslationCharactersUsed.class); + Root root = criteriaQuery.from(MachineTranslationCharactersUsed.class); + criteriaQuery.select(root).where(builder.equal(root.get("id"),1L)); + + MachineTranslationCharactersUsed used = session.createQuery(criteriaQuery).uniqueResult(); if (used != null && used.getMonth().equals(getCurrentMonth())) { return used.getUsed(); } else { diff --git a/amp/src/main/java/org/dgfoundation/amp/menu/MenuConstants.java b/amp/src/main/java/org/dgfoundation/amp/menu/MenuConstants.java index fed2fc43e24..319e088b751 100644 --- a/amp/src/main/java/org/dgfoundation/amp/menu/MenuConstants.java +++ b/amp/src/main/java/org/dgfoundation/amp/menu/MenuConstants.java @@ -46,6 +46,9 @@ public class MenuConstants { public static final String ADD_TAB = "Add Tab"; public static final String TAB_MANAGER = "Tab Manager"; public static final String IATI_IMPORTER = "IATI Importer"; + public static final String DATA_IMPORTER = "Data Importer"; + public static final String IMPORT_DATA = "Import Data"; + public static final String VIEW_PROGRESS = "View Progress"; // TBD how change workspace will work public static final String CHANGE_WORKSPACE = "Change Workspace"; public static final String PLEDGES = "Pledges"; diff --git a/amp/src/main/java/org/dgfoundation/amp/menu/MenuItemsProcessor.java b/amp/src/main/java/org/dgfoundation/amp/menu/MenuItemsProcessor.java index 5d30cdc9e6e..4b250f77c9e 100644 --- a/amp/src/main/java/org/dgfoundation/amp/menu/MenuItemsProcessor.java +++ b/amp/src/main/java/org/dgfoundation/amp/menu/MenuItemsProcessor.java @@ -39,6 +39,7 @@ public static List processForCurrentRequest(List items, AmpV private AmpView view; private TeamMember tm; + private AmpTeamMember currentAmpTeamMember; private Set currentUserGroupKeys = new HashSet(); private String referer; @@ -51,8 +52,9 @@ private MenuItemsProcessor(AmpView view) { currentUserGroupKeys.add(Group.PLEDGERS); } if (tm != null && tm.getMemberId() != null) { - AmpTeamMember atm = TeamMemberUtil.getAmpTeamMember(tm.getMemberId()); - Set userGroups = atm != null && atm.getUser() != null ? atm.getUser().getGroups() : null; + this.currentAmpTeamMember = TeamMemberUtil.getAmpTeamMember(tm.getMemberId()); + Set userGroups = currentAmpTeamMember != null && currentAmpTeamMember.getUser() != null + ? currentAmpTeamMember.getUser().getGroups() : null; if (userGroups != null) { for(Group group : userGroups) { currentUserGroupKeys.add(group.getKey()); @@ -102,6 +104,11 @@ private boolean isVisible(MenuItem item) { case MenuConstants.IATI_IMPORTER: visible = canAddActivity() || MenuUtils.isAmpAdmin(); break; + case MenuConstants.DATA_IMPORTER: + case MenuConstants.IMPORT_DATA: + case MenuConstants.VIEW_PROGRESS: + visible = canAccessDataImporter(); + break; case MenuConstants.GPI_DATA: visible = FeaturesUtil.isVisibleModule(MenuConstants.GPI_DATA_ENTRY); break; @@ -116,6 +123,17 @@ private boolean canAddActivity() { return tm != null && Boolean.TRUE.equals(tm.getAddActivity()); } + private boolean canAccessDataImporter() { + if (MenuUtils.isAmpAdmin()) { + return true; + } + if (currentAmpTeamMember == null || currentAmpTeamMember.getAmpMemberRole() == null) { + return false; + } + return Boolean.TRUE.equals(currentAmpTeamMember.getAmpMemberRole().getTeamHead()) + || currentAmpTeamMember.getAmpMemberRole().isApprover(); + } + private boolean isAllowedUserGroup(MenuItem mi) { if (mi.groupKeys == null || mi.groupKeys.size() == 0 || AmpView.ADMIN == view && CollectionUtils.containsAny(mi.groupKeys, Group.ADMIN_GROUPS)) { diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/OnePagerConst.java b/amp/src/main/java/org/dgfoundation/amp/onepager/OnePagerConst.java index 57f4a3de2e3..5bb0ce130c2 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/OnePagerConst.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/OnePagerConst.java @@ -5,10 +5,7 @@ package org.dgfoundation.amp.onepager; import org.apache.wicket.MetaDataKey; -import org.dgfoundation.amp.onepager.helper.EditorStore; -import org.dgfoundation.amp.onepager.helper.ResourceTranslationStore; -import org.dgfoundation.amp.onepager.helper.TemporaryActivityDocument; -import org.dgfoundation.amp.onepager.helper.TemporaryGPINiDocument; +import org.dgfoundation.amp.onepager.helper.*; import org.digijava.kernel.startup.AmpSessionListener; import org.digijava.module.aim.dbentity.*; @@ -34,8 +31,11 @@ public final class OnePagerConst { public static final MetaDataKey> RESOURCES_NEW_ITEMS = new MetaDataKey>(){}; + public static final MetaDataKey>> COMPONENT_FUNDING_NEW_ITEMS = new MetaDataKey>>(){}; public static final MetaDataKey> RESOURCES_DELETED_ITEMS = new MetaDataKey>(){}; + public static final MetaDataKey>> COMPONENT_FUNDING_DELETED_ITEMS = new MetaDataKey>>(){}; public static final MetaDataKey> RESOURCES_EXISTING_ITEM_TITLES = new MetaDataKey>(){}; + public static final MetaDataKey>> COMPONENT_FUNDING_EXISTING_ITEM_TITLES = new MetaDataKey>>(){}; public static final MetaDataKey> RESOURCES_TRANSLATIONS = new MetaDataKey>(){}; public static final MetaDataKey> GPI_RESOURCES_NEW_ITEMS = new MetaDataKey>(){}; @@ -75,6 +75,7 @@ public final class OnePagerConst { final static String toggleJS= "$('#%s').click(function(){$(this).siblings('div:first').slideToggle();return false;})"; final static String toggleChildrenJS = "$('#%s').find('a.slider').click(function(){$(this).siblings('div:first').slideToggle();return false;})"; + final static String toggleChildrenJSComponent = "$('#%s').find('a.slider').click(function(){$(this).closest('tr.wicketFundingRowItem').find('#%s').slideToggle();return false;})"; final static String clickToggleJS= "$('#%s').siblings('div:first').slideToggle();"; final static String clickToggle2JS= "$('#%s').find('div:first').find('div:first').slideToggle();"; final static String toggleJSPM ="$(document).ready(function(){$('#%s').click(function(){$(this).siblings('div:first').slideToggle();return false;});})"; diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/OnePagerUtil.java b/amp/src/main/java/org/dgfoundation/amp/onepager/OnePagerUtil.java index 98882f6f1ab..4b1932f5732 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/OnePagerUtil.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/OnePagerUtil.java @@ -185,6 +185,10 @@ public static String getToggleJSPM(Component c) { return String.format(OnePagerConst.toggleJSPM, c.getMarkupId()); } + public static String getToggleChildrenJSComponentFunding(Component c, String id) + { + return String.format(OnePagerConst.toggleChildrenJSComponent,c.getMarkupId(),id); + } /** * Slide the section on refresh diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/AmpActivityFormFeature.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/AmpActivityFormFeature.java index d04c46e95a1..bf1ee53cbc3 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/AmpActivityFormFeature.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/AmpActivityFormFeature.java @@ -36,6 +36,7 @@ import org.apache.wicket.request.flow.RedirectToUrlException; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.request.resource.PackageResourceReference; +import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.util.time.Duration; import org.apache.wicket.util.visit.IVisit; import org.apache.wicket.util.visit.IVisitor; @@ -75,6 +76,7 @@ import org.digijava.module.aim.util.DbUtil; import org.digijava.module.aim.util.FeaturesUtil; import org.digijava.module.aim.util.TeamMemberUtil; +import org.digijava.module.aim.util.TeamUtil; import org.digijava.module.message.dbentity.AmpAlert; import org.digijava.module.message.dbentity.AmpMessage; import org.digijava.module.message.dbentity.AmpMessageState; @@ -83,6 +85,8 @@ import org.digijava.module.message.triggers.ApprovedActivityTrigger; import org.digijava.module.message.triggers.NotApprovedActivityTrigger; import org.digijava.module.message.util.AmpMessageUtil; +import org.digijava.module.trubudget.dbentity.TruBudgetActivity; +import org.digijava.module.trubudget.util.ProjectUtil; import java.util.*; @@ -102,6 +106,13 @@ public class AmpActivityFormFeature extends AmpFeaturePanel private static final Integer GO_TO_DESKTOP=1; private static final Integer STAY_ON_PAGE=2; private AbstractAjaxTimerBehavior autoSaveTimer; + private IModel pendingSaveActivityModel; + private FeedbackPanel pendingSaveFeedbackPanel; + private Model pendingSaveRedirected; + private boolean pendingSaveDraft; + private boolean pendingSaveRejected; + private boolean pendingSendRejectMessage; + private IModel pendingRejectMessageActivityModel; public Form getActivityForm() { return activityForm; } @@ -376,12 +387,77 @@ public void onClick(AjaxRequestTarget target) { public void onClick(AjaxRequestTarget target) { target.appendJavaScript("hideWarningPanel();"); //TODO: fix draft param - saveMethod(target, am, feedbackPanel, false, redirected,false); + saveMethodWithTruBudgetConfirmation(target, am, feedbackPanel, false, redirected, false, + false, null); } }; saveButton.add(new AttributeModifier("value", TranslatorUtil.getTranslatedText("Save"))); add(saveButton); + IndicatingAjaxLink trubudgetCancelButton = new IndicatingAjaxLink("trubudgetPanelCancel") { + @Override + public void onClick(AjaxRequestTarget target) { + clearPendingSave(); + target.appendJavaScript("hideTruBudgetClosePanel();enableButtons2();"); + } + }; + trubudgetCancelButton.add(new AttributeModifier("value", TranslatorUtil.getTranslatedText("Cancel"))); + add(trubudgetCancelButton); + + IndicatingAjaxLink trubudgetSaveButton = new IndicatingAjaxLink("trubudgetPanelSave") { + @Override + public void onClick(AjaxRequestTarget target) { + boolean closeProjectOnTruBudget = RequestCycle.get().getRequest().getRequestParameters() + .getParameterValue("closeProjectOnTruBudget").toBoolean(false); + + target.appendJavaScript("hideTruBudgetClosePanel();"); + + if (pendingSaveActivityModel != null && pendingSaveFeedbackPanel != null && pendingSaveRedirected != null) { + saveMethod(target, pendingSaveActivityModel, pendingSaveFeedbackPanel, pendingSaveDraft, + pendingSaveRedirected, pendingSaveRejected, closeProjectOnTruBudget); + } + + if (pendingSendRejectMessage && pendingRejectMessageActivityModel != null) { + sendRejectMessage(pendingRejectMessageActivityModel); + } + + clearPendingSave(); + } + + @Override + protected void updateAjaxAttributes(AjaxRequestAttributes attributes) { + super.updateAjaxAttributes(attributes); + attributes.getDynamicExtraParameters().add( + "return { closeProjectOnTruBudget: $('#closeProjectOnTruBudgetCheckbox').is(':checked') };" + ); + } + }; + trubudgetSaveButton.add(new AttributeModifier("value", TranslatorUtil.getTranslatedText("Save"))); + add(trubudgetSaveButton); + + IndicatingAjaxLink trubudgetAlreadyClosedOkButton = + new IndicatingAjaxLink("trubudgetAlreadyClosedPanelOk") { + @Override + public void onClick(AjaxRequestTarget target) { + target.appendJavaScript("hideTruBudgetAlreadyClosedPanel();"); + + if (pendingSaveActivityModel != null && pendingSaveFeedbackPanel != null + && pendingSaveRedirected != null) { + saveMethod(target, pendingSaveActivityModel, pendingSaveFeedbackPanel, pendingSaveDraft, + pendingSaveRedirected, pendingSaveRejected, false); + } + + if (pendingSendRejectMessage && pendingRejectMessageActivityModel != null) { + sendRejectMessage(pendingRejectMessageActivityModel); + } + + clearPendingSave(); + } + }; + trubudgetAlreadyClosedOkButton + .add(new AttributeModifier("value", TranslatorUtil.getTranslatedText("OK"))); + add(trubudgetAlreadyClosedOkButton); + //add ajax submit button final AmpButtonField saveAndSubmit = new AmpButtonField("saveAndSubmit","Save and Submit", AmpFMTypes.MODULE, true) { @@ -421,7 +497,8 @@ protected void onSubmit(final AjaxRequestTarget target, Form form) { } else{ - saveMethod(target, am, feedbackPanel, false, redirected,false); + saveMethodWithTruBudgetConfirmation(target, am, feedbackPanel, false, redirected, false, + false, null); } } else{ @@ -676,7 +753,7 @@ protected void onSubmit(AjaxRequestTarget target) { if (!activityForm.hasError()) { redirected.setObject(STAY_ON_PAGE); - saveMethod(target, am, feedbackPanel, true, redirected,false); + saveMethod(target, am, feedbackPanel, true, redirected, false, false); } else { formSubmitErrorHandle(activityForm, target, feedbackPanel); @@ -937,18 +1014,17 @@ private void verifySections(IModel am, boolean alertIfDisbur } //Regional Funding - Set regionalSet = activity.getRegionalFundings(); + Set regionalSet = activity.getRegionalFundings(); if (regionalSet != null){ HashSet verifiedRegions = new HashSet(); - for (Iterator iterator = regionalSet.iterator(); iterator.hasNext(); ){ - AmpRegionalFunding funding = iterator.next(); + for (AmpRegionalFunding funding : regionalSet) { if (funding.getRegionLocation() == null || verifiedRegions.contains(funding.getRegionLocation().getId())) continue; verifiedRegions.add(funding.getRegionLocation().getId()); - verifySet(new PropertyModel(am, "regionalFundings"), alertIfDisbursementBiggerCommitments, + verifySet(new PropertyModel<>(am, "regionalFundings"), alertIfDisbursementBiggerCommitments, alertIfExpenditureBiggerDisbursement, commitmentErrors, expenditureErrors, funding.getRegionLocation(), TranslatorUtil.getTranslatedText(OnePager.REGIONAL_FUNDING_SECTION_NAME) + ": " + - funding.getRegionLocation().getAutoCompleteLabel()); + funding.getRegionLocation().getAutoCompleteLabel()); } } @@ -957,7 +1033,7 @@ private void verifySections(IModel am, boolean alertIfDisbur if (componentSet != null) { for (AmpComponent component : componentSet) { for (AmpComponentFunding funding : component.getFundings()) { - verifySet(new PropertyModel(component, "fundings"), alertIfDisbursementBiggerCommitments, + verifySet(new PropertyModel<>(component, "fundings"), alertIfDisbursementBiggerCommitments, alertIfExpenditureBiggerDisbursement, commitmentErrors, expenditureErrors, funding.getComponent(), TranslatorUtil.getTranslatedText(OnePager.COMPONENTS_SECTION_NAME) + ": " + funding.getComponent().getTitle()); @@ -980,7 +1056,7 @@ private void verifySet(PropertyModel detailsModel, boolean alertIfDisbursem if (alertIfExpenditureBiggerDisbursement){ double expenditureSum = sumUp(details, Constants.EXPENDITURE, parent); if (expenditureSum > disbursementSum) - expenditureErrors.put(itemIdentifier, Double.toString(expenditureSum) + " > " + Double.toString(disbursementSum)); + expenditureErrors.put(itemIdentifier, expenditureSum + " > " + disbursementSum); } } @@ -1103,9 +1179,120 @@ public void component(FormComponent component, IVisit visit) { target.add(feedbackPanel); } + private void saveMethodWithTruBudgetConfirmation(AjaxRequestTarget target, + IModel am, + FeedbackPanel feedbackPanel, + boolean draft, + Model redirected, + boolean rejected, + boolean sendRejectMessageAfterSave, + IModel rejectMessageActivityModel) { + if (shouldShowTruBudgetAlreadyClosedPopup(am, draft, rejected)) { + this.pendingSaveActivityModel = am; + this.pendingSaveFeedbackPanel = feedbackPanel; + this.pendingSaveRedirected = redirected; + this.pendingSaveDraft = draft; + this.pendingSaveRejected = rejected; + this.pendingSendRejectMessage = sendRejectMessageAfterSave; + this.pendingRejectMessageActivityModel = rejectMessageActivityModel; + target.appendJavaScript("showTruBudgetAlreadyClosedPanel();"); + return; + } + + if (shouldShowTruBudgetCloseConfirmation(am, draft, rejected)) { + this.pendingSaveActivityModel = am; + this.pendingSaveFeedbackPanel = feedbackPanel; + this.pendingSaveRedirected = redirected; + this.pendingSaveDraft = draft; + this.pendingSaveRejected = rejected; + this.pendingSendRejectMessage = sendRejectMessageAfterSave; + this.pendingRejectMessageActivityModel = rejectMessageActivityModel; + target.appendJavaScript("showTruBudgetClosePanel();"); + return; + } + + saveMethod(target, am, feedbackPanel, draft, redirected, rejected, false); + if (sendRejectMessageAfterSave && rejectMessageActivityModel != null) { + sendRejectMessage(rejectMessageActivityModel); + } + } + + private boolean shouldShowTruBudgetAlreadyClosedPopup(IModel am, + boolean draft, + boolean rejected) { + if (!isTruBudgetCloseFlowEligible(am, draft, rejected)) { + return false; + } + + TruBudgetActivity truBudgetActivity = getTruBudgetActivity(am); + return truBudgetActivity != null && Boolean.TRUE.equals(truBudgetActivity.getProjectClosed()); + } + + private boolean shouldShowTruBudgetCloseConfirmation(IModel am, + boolean draft, + boolean rejected) { + if (!isTruBudgetCloseFlowEligible(am, draft, rejected)) { + return false; + } + + AmpActivityVersion activity = am != null ? am.getObject() : null; + String ampId = activity != null ? activity.getAmpId() : null; + if (ampId == null || ampId.trim().isEmpty()) { + return true; + } + + TruBudgetActivity truBudgetActivity = getTruBudgetActivity(am); + return truBudgetActivity == null || !Boolean.TRUE.equals(truBudgetActivity.getProjectClosed()); + } + + private TruBudgetActivity getTruBudgetActivity(IModel am) { + AmpActivityVersion activity = am != null ? am.getObject() : null; + String ampId = activity != null ? activity.getAmpId() : null; + if (ampId == null || ampId.trim().isEmpty()) { + return null; + } + return ProjectUtil.activityAlreadyInTrubudget(ampId); + } + + private boolean isTruBudgetCloseFlowEligible(IModel am, + boolean draft, + boolean rejected) { + if (draft) { + return false; + } + + AmpActivityVersion activity = am != null ? am.getObject() : null; + ApprovalStatus approvalStatus = activity != null ? activity.getApprovalStatus() : null; + boolean isApprovedOrRejected = ApprovalStatus.approved.equals(approvalStatus) + || ApprovalStatus.rejected.equals(approvalStatus); + + if (!rejected && !isApprovedOrRejected) { + return false; + } + + User currentUser = TeamUtil.getCurrentUser(); + return currentUser != null + && currentUser.getTruBudgetEnabled() + && org.digijava.module.um.util.DbUtil + .getSettingValue(org.digijava.module.um.util.DbUtil.getGlobalSettingsBySection("trubudget"), + "isEnabled") + .equalsIgnoreCase("true"); + } + + private void clearPendingSave() { + this.pendingSaveActivityModel = null; + this.pendingSaveFeedbackPanel = null; + this.pendingSaveRedirected = null; + this.pendingSaveDraft = false; + this.pendingSaveRejected = false; + this.pendingSendRejectMessage = false; + this.pendingRejectMessageActivityModel = null; + } + protected void saveMethod(AjaxRequestTarget target, IModel am, FeedbackPanel feedbackPanel, - boolean draft, Model redirected,boolean rejected) { + boolean draft, Model redirected, boolean rejected, + boolean closeProjectOnTruBudget) { AmpActivityModel a = (AmpActivityModel) am; AmpActivityVersion activity = am.getObject(); @@ -1125,7 +1312,7 @@ protected void saveMethod(AjaxRequestTarget target, throw new RedirectToUrlException(ActivityGatekeeper.buildRedirectLink(String.valueOf(a.getId()), currentUserId)); } - ActivityUtil.saveActivity((AmpActivityModel) am, draft, rejected); + ActivityUtil.saveActivity((AmpActivityModel) am, draft, rejected, closeProjectOnTruBudget); info(TranslatorUtil.getTranslatedText("Activity saved successfully")); @@ -1287,11 +1474,8 @@ protected void onSaveAsDraftOrReject(final IModel am, final } else { //if no error happend and we are rejecting we change the approval status to rejected // send a message to the creator of the activity - saveMethod(target, am, feedbackPanel, true, redirected, isRejected); - - if (isRejected) { - sendRejectMessage(am); - } + saveMethodWithTruBudgetConfirmation(target, am, feedbackPanel, true, redirected, isRejected, + isRejected, am); } target.appendJavaScript("enableButtons2();"); diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/saveNavigationPanel.js b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/saveNavigationPanel.js index 1f69360b0c7..0ca1fa33115 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/saveNavigationPanel.js +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/saveNavigationPanel.js @@ -90,3 +90,72 @@ function hideRejectActivityPanel(){ rejectActivityPanel.hide(); } } + +var trubudgetClosePanel; +function showTruBudgetClosePanel(){ + if(trubudgetClosePanel==null){ + trubudgetClosePanel = new YAHOOAmp.widget.Panel("trubudgetClosePanel", { + width : "420px", + fixedcenter : true, + constraintoviewport : true, + underlay : "shadow", + close : true, + visible : true, + modal : true, + draggable : false + }); + + $("#closeProjectOnTruBudgetCheckbox").prop("checked", false); + $("#trubudgetClosePanel").show(); + + trubudgetClosePanel.beforeHideEvent.subscribe(function() { + enableButtons2(); + }); + trubudgetClosePanel.render(); + } + else{ + $("#closeProjectOnTruBudgetCheckbox").prop("checked", false); + trubudgetClosePanel.show(); + } + + return false; +} + +function hideTruBudgetClosePanel(){ + if(trubudgetClosePanel!=null){ + trubudgetClosePanel.hide(); + } +} + +var trubudgetAlreadyClosedPanel; +function showTruBudgetAlreadyClosedPanel(){ + if(trubudgetAlreadyClosedPanel==null){ + trubudgetAlreadyClosedPanel = new YAHOOAmp.widget.Panel("trubudgetAlreadyClosedPanel", { + width : "420px", + fixedcenter : true, + constraintoviewport : true, + underlay : "shadow", + close : true, + visible : true, + modal : true, + draggable : false + }); + + $("#trubudgetAlreadyClosedPanel").show(); + trubudgetAlreadyClosedPanel.beforeHideEvent.subscribe(function() { + enableButtons2(); + }); + trubudgetAlreadyClosedPanel.render(); + } + else{ + trubudgetAlreadyClosedPanel.show(); + } + + return false; +} + +function hideTruBudgetAlreadyClosedPanel(){ + if(trubudgetAlreadyClosedPanel!=null){ + trubudgetAlreadyClosedPanel.hide(); + } +} diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/sections/AmpComponentsFormSectionFeature.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/sections/AmpComponentsFormSectionFeature.java index 349c5eeda58..e3b9c13f4bf 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/sections/AmpComponentsFormSectionFeature.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/sections/AmpComponentsFormSectionFeature.java @@ -19,6 +19,7 @@ import org.digijava.module.aim.helper.GlobalSettingsConstants; import org.digijava.module.aim.util.ComponentsUtil; import org.digijava.module.aim.util.FeaturesUtil; +import org.digijava.module.categorymanager.util.CategoryConstants; import java.util.List; import java.util.Set; @@ -35,9 +36,9 @@ public class AmpComponentsFormSectionFeature extends public AmpComponentsFormSectionFeature(String id, String fmName, final IModel am) throws Exception { super(id, fmName, am); - final PropertyModel> setModel=new PropertyModel>(am,"components"); + final PropertyModel> setModel= new PropertyModel<>(am, "components"); if (setModel.getObject() == null) - setModel.setObject(new TreeSet()); + setModel.setObject(new TreeSet<>()); final ListEditor list; IModel> listModel = OnePagerUtil.getReadOnlyListModelFromSetModel(setModel); @@ -72,6 +73,7 @@ public void onClick(AjaxRequestTarget target) { } comp.setType(defaultType); + comp.setComponentStatus(CategoryConstants.COMPONENT_STATUS_OPEN.getAmpCategoryValueFromDB()); target.add(this.getParent()); target.appendJavaScript(OnePagerUtil.getToggleChildrenJS(this.getParent())); } diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/subsections/AmpComponentsFundingSubsectionFeature.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/subsections/AmpComponentsFundingSubsectionFeature.java index d5c842fe469..d177bdd393d 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/subsections/AmpComponentsFundingSubsectionFeature.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/subsections/AmpComponentsFundingSubsectionFeature.java @@ -17,6 +17,7 @@ import org.digijava.module.categorymanager.util.CategoryConstants; import java.util.Set; +import java.util.UUID; /** * @author aartimon@dginternational.org @@ -62,12 +63,14 @@ public AmpComponentsFundingSubsectionFeature(String id, public void onClick(AjaxRequestTarget target) { AmpComponentFunding cf = new AmpComponentFunding(); cf.setAdjustmentType(CategoryConstants.ADJUSTMENT_TYPE_ACTUAL.getAmpCategoryValueFromDB()); + cf.setComponentFundingStatus(CategoryConstants.COMPONENT_FUNDING_STATUS_OPEN.getAmpCategoryValueFromDB()); cf.setComponent(componentModel.getObject()); cf.setTransactionAmount(0d); cf.setCurrency(CurrencyUtil.getWicketWorkspaceCurrency()); cf.setTransactionDate(null); cf.setTransactionType(transactionType); cf.setAmpComponentFundingId(null); + cf.setJustAnId(UUID.randomUUID().toString()); tableFeature.getEditorList().addItem(cf); diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpComponentFundingResourcesTableFeature.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpComponentFundingResourcesTableFeature.java new file mode 100644 index 00000000000..922f4876a05 --- /dev/null +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpComponentFundingResourcesTableFeature.java @@ -0,0 +1,314 @@ +package org.dgfoundation.amp.onepager.components.features.tables; + + +import org.apache.wicket.AttributeModifier; +import org.apache.wicket.MetaDataKey; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.markup.html.AjaxLink; +import org.apache.wicket.markup.html.WebMarkupContainer; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.form.Button; +import org.apache.wicket.markup.html.link.ExternalLink; +import org.apache.wicket.markup.html.link.Link; +import org.apache.wicket.markup.html.list.ListItem; +import org.apache.wicket.markup.html.list.ListView; +import org.apache.wicket.model.AbstractReadOnlyModel; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.model.PropertyModel; +import org.apache.wicket.protocol.http.WebApplication; +import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler; +import org.apache.wicket.util.file.File; +import org.dgfoundation.amp.onepager.OnePagerConst; +import org.dgfoundation.amp.onepager.components.fields.AmpTextFieldPanel; +import org.dgfoundation.amp.onepager.helper.DownloadResourceStream; +import org.dgfoundation.amp.onepager.helper.TemporaryComponentFundingDocument; +import org.dgfoundation.amp.onepager.helper.TemporaryDocument; +import org.dgfoundation.amp.onepager.models.PersistentObjectModel; +import org.dgfoundation.amp.onepager.models.ResourceTranslationModel; +import org.dgfoundation.amp.onepager.util.AmpFMTypes; +import org.dgfoundation.amp.onepager.util.SessionUtil; +import org.digijava.kernel.translator.TranslatorWorker; +import org.digijava.module.aim.dbentity.AmpComponentFunding; +import org.digijava.module.aim.dbentity.AmpComponentFundingDocument; +import org.digijava.module.aim.helper.Constants; +import org.digijava.module.aim.util.FeaturesUtil; +import org.digijava.module.contentrepository.helper.NodeWrapper; +import org.digijava.module.contentrepository.util.DocumentManagerUtil; +import org.digijava.module.translation.util.ContentTranslationUtil; + +import javax.jcr.Node; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * @author aartimon@dginternational.org + * @since Apr 13, 2011 + */ +public class AmpComponentFundingResourcesTableFeature extends AmpFormTableFeaturePanel { + + boolean refreshExistingDocs = false; + + + /** + * @param id + * @param fmName + * @param am + * @throws Exception + */ + public AmpComponentFundingResourcesTableFeature(String id, String fmName, + final IModel am) throws Exception { + super(id, am, fmName); + super.setTitleHeaderColSpan(10); +// getSession().setMetaData(OnePagerConst.COMPONENT_FUNDING_NEW_ITEMS, new HashSet<>()); +// getSession().setMetaData(OnePagerConst.COMPONENT_FUNDING_DELETED_ITEMS, new HashSet<>()); +// getSession().setMetaData(OnePagerConst.COMPONENT_FUNDING_EXISTING_ITEM_TITLES, new HashSet<>()); + + + final IModel> setModel = new PropertyModel<>(am, "componentFundingDocuments"); + +// if (am.getObject().getComponentFundingDocuments() == null) +// am.getObject().getComponentFundingDocuments().addAll(new HashSet<>()); + + IModel> listModel = new AbstractReadOnlyModel>() { + + private transient List existingTmpDocs = getExistingObject(); + + private List getExistingObject() { + Iterator it = setModel.getObject().iterator(); + List ret = new ArrayList<>(); + HashSet existingDocTitles = new HashSet<>(); + + while (it.hasNext()) { + AmpComponentFundingDocument d = it.next(); + Node node = DocumentManagerUtil.getWriteNode(d.getUuid(), SessionUtil.getCurrentServletRequest()); + NodeWrapper nw = new NodeWrapper(node); + + if (node == null) + continue; + + /** + * Code to add TempDoc to list + */ + TemporaryComponentFundingDocument td = new TemporaryComponentFundingDocument(); + td.setExisting(true); + td.setExistingDocument(d); + td.setDate(nw.getCalendarDate()); + td.setDescription(nw.getDescription()); + td.setNote(nw.getNotes()); + td.setTitle(nw.getTitle()); +// td.setType(CategoryManagerUtil.getAmpCategoryValueFromDb(nw.getCmDocTypeId())); + td.setWebLink(nw.getWebLink()); + td.setYear(nw.getYearOfPublication()); + td.setFileSize(nw.getFileSizeInMegabytes()); + td.setFileName(nw.getName()); +// td.setLabels(nw.getLabels()); + td.setContentType(nw.getContentType()); + + ret.add(td); + + // Existing doc titles should be populated only when multilingual is enabled + if (ContentTranslationUtil.multilingualIsEnabled()) { + TemporaryComponentFundingDocument titleHolder = new TemporaryComponentFundingDocument(); + titleHolder.setTitle(td.getTitle()); + titleHolder.setExistingDocument(d); + existingDocTitles.add(titleHolder); + existingDocTitles.add(td); + } + } + String justAnId= am.getObject().getJustAnId(); + + MetaDataKey>> metaDataKey = OnePagerConst.COMPONENT_FUNDING_EXISTING_ITEM_TITLES; + + HashMap> metaData = getSession().getMetaData(metaDataKey); + + if (metaData == null) { + metaData = new HashMap<>(); + + } + getSession().setMetaData(metaDataKey, metaData); + + HashSet existingSet = metaData.computeIfAbsent(justAnId, k -> new HashSet<>()); + + existingSet.addAll(existingDocTitles); + refreshExistingDocs = false; + return ret; + } + + + + @Override + public List getObject() { + HashSet newItems = getSession().getMetaData(OnePagerConst.COMPONENT_FUNDING_NEW_ITEMS).get(am.getObject().getJustAnId()); + if (newItems == null) + newItems = new HashSet<>(); + HashMap> delItemsMap = getSession().getMetaData(OnePagerConst.COMPONENT_FUNDING_DELETED_ITEMS); + HashSet delItems = delItemsMap.get(am.getObject().getJustAnId()); + if (delItems == null) + delItems = new HashSet<>(); + + if (refreshExistingDocs) + existingTmpDocs = getExistingObject(); + if (existingTmpDocs==null){ + existingTmpDocs=new ArrayList<>(); + } + List ret = new ArrayList<>(existingTmpDocs); + +// if (am.getObject().getComponentFundingDocuments() == null) +// am.getObject().getComponentFundingDocuments().addAll(new HashSet<>()); + + for (AmpComponentFundingDocument d : setModel.getObject()) { + //check if marked for delete + if (delItems.contains(d)) { + for (TemporaryDocument td : existingTmpDocs) { + if (td.getExistingDocument().equals(d)) { + existingTmpDocs.remove(td); + break; + } + } + } + } + ret.addAll(newItems); + + return ret; + } + }; + + + list = new ListView("componentFundingDocumentList", listModel) { + private static final long serialVersionUID = 7218457979728871528L; + + @Override + protected void onAfterRender() { + super.onAfterRender(); + DocumentManagerUtil.logoutJcrSessions(SessionUtil.getCurrentServletRequest()); + } + + @Override + protected void populateItem(final ListItem item) { + if (item.getModel() == null && item.getModelObject() == null) { + logger.info("yoh here"); + return; + } + + TemporaryDocument document = item.getModelObject(); + + if (!ContentTranslationUtil.multilingualIsEnabled()) { + item.add(new Label("componentFundingDocumentTitle", item.getModel().getObject().getTitle())); + } else { + String id; + if (document.getExistingDocument() != null) { + id = document.getExistingDocument().getUuid(); + } else { + id = document.getNewTemporaryDocumentId(); + } + Model newResourceIdModel = new Model(id); + final ResourceTranslationModel titleModel = new ResourceTranslationModel(new PropertyModel<>(item.getModel().getObject(), "title"), newResourceIdModel); + final AmpTextFieldPanel name = new AmpTextFieldPanel("componentFundingDocumentTitle", titleModel, "Title", AmpFMTypes.MODULE, Boolean.TRUE); + name.setEnabled(false); + name.getTextContainer().setRequired(true); + item.add(name); + + + } + + if (item.getModel().getObject().getFileName() == null) { + item.add(new Label("componentFundingDocumentResourceName", item.getModel().getObject().getWebLink())); + } else { + item.add(new Label("componentFundingDocumentResourceName", item.getModel().getObject().getFileName())); + } + + AjaxLink delComponentDoc = new AjaxLink("componentFundingDocumentDelete", setModel) { + @Override + public void onClick(AjaxRequestTarget target) { + if (item.getModelObject().isExisting()) { + MetaDataKey>> metaDataKey = OnePagerConst.COMPONENT_FUNDING_DELETED_ITEMS; + + HashMap> metaData = getSession().getMetaData(metaDataKey); + + if (metaData == null) { + metaData = new HashMap<>(); + + } + getSession().setMetaData(metaDataKey, metaData); + HashSet delItems = metaData.computeIfAbsent(am.getObject().getJustAnId(), k -> new HashSet<>()); + delItems.add(item.getModelObject().getExistingDocument()); + } else { + HashSet newItems = getSession().getMetaData(OnePagerConst.COMPONENT_FUNDING_NEW_ITEMS).get(am.getObject().getJustAnId()); + newItems.remove(item.getModelObject()); + } + target.add(list.getParent()); + } + }; + // TODO: 10/2/23 add some style to + delComponentDoc.add(new Button("deleteComponentFundingDocButton").add(new Label("deleteComponentFundingDocLabel", TranslatorWorker.translateText("Del")))); + item.add(delComponentDoc); +// item.add(new ListEditorRemoveButton("componentFundingDocumentDelete", "Component Funding Document Delete Resource")); + + PropertyModel dateModel = new PropertyModel<>(item.getModel(), "date.time"); + String pattern = FeaturesUtil.getGlobalSettingValue(Constants.GLOBALSETTINGS_DATEFORMAT); + pattern = pattern.replace('m', 'M'); + SimpleDateFormat formater = new SimpleDateFormat(pattern); + String formatedDate = formater.format(dateModel.getObject()); + + item.add(new Label("componentFundingDocumentDate", formatedDate)); + item.add(new Label("componentFundingDocumentYear",item.getModel().getObject().getYear())); + item.add(new Label("componentFundingDocumentSize", item.getModel().getObject().getFileSize())); +// item.add(new AmpLabelFieldPanel("docType", new PropertyModel(item.getModel(), "type.label"), "Document Type", true)); + + final DownloadResourceStream drs; + if (item.getModelObject().isExisting()) + drs = new DownloadResourceStream(new PersistentObjectModel<> + (item.getModelObject().getExistingDocument()), item.getModelObject().getFileName()); + else + drs = new DownloadResourceStream(item.getModelObject().getFile(), item.getModelObject().getFileName()); + + String webLink = item.getModelObject().getWebLink(); + + if (webLink != null && webLink.length() > 0) { + if (!webLink.startsWith("http")) + webLink = "http://" + webLink; + ExternalLink link = new ExternalLink("componentFundingDocumentDownload", new Model<>(webLink)); + item.add(link); + WebMarkupContainer downloadLinkImg = new WebMarkupContainer("componentFundingDocumentDownloadImage"); + downloadLinkImg.add(new AttributeModifier("src", new Model<>("/TEMPLATE/ampTemplate/img_2/ico_attachment.png"))); + link.add(downloadLinkImg); + } else { + Link downloadLink = new Link("componentFundingDocumentDownload") { + @Override + public void onClick() { + getRequestCycle().scheduleRequestHandlerAfterCurrent(new ResourceStreamRequestHandler(drs, drs.getFileName())); + } + }; + item.add(downloadLink); + + String contentType = item.getModelObject().getFileName(); + int index = contentType.lastIndexOf('.'); + + String extension = contentType.substring(index + 1, contentType.length()); + String extPath = "/TEMPLATE/ampTemplate/images/icons/" + extension + ".gif"; + File extImgFile = new File(WebApplication.get().getServletContext().getRealPath(extPath)); + if (!extImgFile.exists()) + extPath = "/TEMPLATE/ampTemplate/images/icons/default.icon.gif"; + else + extPath = "/TEMPLATE/ampTemplate/images/icons/" + extension + ".gif"; + + WebMarkupContainer downloadLinkImg = new WebMarkupContainer("componentFundingDocumentDownloadImage"); + downloadLinkImg.add(new AttributeModifier("src", new Model<>(extPath))); + downloadLink.add(downloadLinkImg); + } + + + } + }; + + add(list); + + } + + + public void setRefreshExistingDocs(boolean refreshExistingDocs) { + this.refreshExistingDocs = refreshExistingDocs; + } + +} \ No newline at end of file diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpComponentIdentificationFormTableFeature.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpComponentIdentificationFormTableFeature.java index 9ceeda0c55a..ae59beda9c9 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpComponentIdentificationFormTableFeature.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpComponentIdentificationFormTableFeature.java @@ -4,6 +4,7 @@ */ package org.dgfoundation.amp.onepager.components.features.tables; +import org.apache.wicket.AttributeModifier; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.behavior.AttributeAppender; @@ -15,12 +16,14 @@ import org.apache.wicket.model.Model; import org.apache.wicket.model.PropertyModel; import org.dgfoundation.amp.onepager.components.features.subsections.AmpSubsectionFeaturePanel; +import org.dgfoundation.amp.onepager.components.fields.AmpCategorySelectFieldPanel; import org.dgfoundation.amp.onepager.components.fields.AmpSelectFieldPanel; import org.dgfoundation.amp.onepager.components.fields.AmpTextAreaFieldPanel; import org.dgfoundation.amp.onepager.components.fields.AmpTextFieldPanel; import org.dgfoundation.amp.onepager.validators.AmpUniqueComponentTitleValidator; import org.digijava.module.aim.dbentity.*; import org.digijava.module.aim.util.ComponentsUtil; +import org.digijava.module.categorymanager.util.CategoryConstants; import java.util.ArrayList; import java.util.List; @@ -96,6 +99,28 @@ protected void onUpdate(AjaxRequestTarget target) { AmpTextAreaFieldPanel description = new AmpTextAreaFieldPanel("description", new PropertyModel(componentModel, "description"), COMPONENT_DESCRIPTION, false, false, false); add(description); + AmpCategorySelectFieldPanel componentStatus = new AmpCategorySelectFieldPanel( + "componentStatus", CategoryConstants.COMPONENT_STATUS_KEY, + new PropertyModel<>(componentModel, "componentStatus"), + COMPONENT_STATUS, //fmname + false, false, false, null, false); +// componentStatus.getChoiceContainer().add(new AjaxFormComponentUpdatingBehavior("onchange") { +// @Override +// protected void onUpdate(AjaxRequestTarget target) { +// String selectedValue = componentModel.getObject().getComponentStatus().getValue(); +// logger.info("Selected Status: "+selectedValue); +// if ("rejected".equalsIgnoreCase(selectedValue)) { +// target.appendJavaScript("$('#" + rejectReason.getMarkupId() + "').show();"); +// } else { +// target.appendJavaScript("$('#" + rejectReason.getMarkupId() + "').hide();"); +// } +// +// +// } +// }); + componentStatus.getChoiceContainer().setRequired(true); + componentStatus.getChoiceContainer().add(new AttributeModifier("style", "width: 100px;")); + add(componentStatus); } diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpComponentsFundingFormTableFeature.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpComponentsFundingFormTableFeature.java index f7d422cc332..049dee78ffe 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpComponentsFundingFormTableFeature.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpComponentsFundingFormTableFeature.java @@ -5,32 +5,35 @@ package org.dgfoundation.amp.onepager.components.features.tables; import org.apache.wicket.AttributeModifier; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.model.AbstractReadOnlyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.PropertyModel; +import org.dgfoundation.amp.onepager.OnePagerConst; import org.dgfoundation.amp.onepager.components.AmpFundingAmountComponent; import org.dgfoundation.amp.onepager.components.ListEditor; import org.dgfoundation.amp.onepager.components.ListEditorRemoveButton; -import org.dgfoundation.amp.onepager.components.fields.AmpCategorySelectFieldPanel; -import org.dgfoundation.amp.onepager.components.fields.AmpSelectFieldPanel; -import org.dgfoundation.amp.onepager.components.fields.AmpTextFieldPanel; +import org.dgfoundation.amp.onepager.components.fields.*; import org.dgfoundation.amp.onepager.events.FundingOrgListUpdateEvent; import org.dgfoundation.amp.onepager.events.UpdateEventBehavior; import org.dgfoundation.amp.onepager.models.AbstractMixedSetModel; import org.dgfoundation.amp.onepager.models.AmpRelatedOrgsModel; -import org.digijava.module.aim.dbentity.AmpActivityVersion; -import org.digijava.module.aim.dbentity.AmpComponent; -import org.digijava.module.aim.dbentity.AmpComponentFunding; -import org.digijava.module.aim.dbentity.AmpOrganisation; +import org.digijava.kernel.persistence.PersistenceManager; +import org.digijava.kernel.user.User; +import org.digijava.module.aim.dbentity.*; import org.digijava.module.categorymanager.dbentity.AmpCategoryValue; import org.digijava.module.categorymanager.util.CategoryConstants; +import org.digijava.module.trubudget.dbentity.AmpComponentFundingTruWF; +import org.digijava.module.trubudget.model.workflowitem.WorkflowItemDetailsModel; +import org.digijava.module.trubudget.util.ProjectUtil; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Set; +import java.util.*; import static org.digijava.module.aim.annotations.interchange.ActivityFieldsConstants.*; +import static org.digijava.module.trubudget.util.TruBudgetAuthUtil.doActualTruBudgetLogin; +import static org.digijava.module.um.util.DbUtil.getGlobalSettingsBySection; +import static org.digijava.module.um.util.DbUtil.getSettingValue; /** * @author aartimon@dginternational.org @@ -62,10 +65,41 @@ public boolean condition(AmpComponentFunding item) { @Override protected void onPopulateItem(org.dgfoundation.amp.onepager.components.ListItem item) { IModel model = item.getModel(); + // if (model.getObject().getComponentFundingDocuments() == null) +// model.getObject().getComponentFundingDocuments(); + if (getSession().getMetaData(OnePagerConst.COMPONENT_FUNDING_NEW_ITEMS)== null) + getSession().setMetaData(OnePagerConst.COMPONENT_FUNDING_NEW_ITEMS,new HashMap<>()); + if (getSession().getMetaData(OnePagerConst.COMPONENT_FUNDING_DELETED_ITEMS) == null) + getSession().setMetaData(OnePagerConst.COMPONENT_FUNDING_DELETED_ITEMS, new HashMap<>()); + if (getSession().getMetaData(OnePagerConst.COMPONENT_FUNDING_EXISTING_ITEM_TITLES) == null) + getSession().setMetaData(OnePagerConst.COMPONENT_FUNDING_EXISTING_ITEM_TITLES, new HashMap<>()); +// item.add(new AttributeModifier("style","")) + User user = model.getObject().getComponent().getActivity().getActivityCreator().getUser(); + if (getSettingValue(getGlobalSettingsBySection("trubudget"),"isEnabled").equalsIgnoreCase("true")&& user.getTruBudgetEnabled()) { + if (model.getObject().getTransactionType()==1) { + PersistenceManager.getRequestDBSession().createQuery("FROM " + AmpComponentFundingTruWF.class.getName() + " act WHERE act.ampComponentFundingId= '" + model.getObject().getJustAnId() + "' AND act.ampComponentFundingId IS NOT NULL", AmpComponentFundingTruWF.class).stream().findAny().ifPresent(ampComponentFundingTruWF->{ + WorkflowItemDetailsModel workflowItemDetailsModel = null; + try { + List settings = getGlobalSettingsBySection("trubudget"); + String token = ProjectUtil.getTrubudgetToken(); + workflowItemDetailsModel = ProjectUtil.getWFItemDetails(ampComponentFundingTruWF,settings,token); + } catch (Exception e) { + logger.info("Error when getting WF details: ",e); + } + if (workflowItemDetailsModel!=null && workflowItemDetailsModel.getData()!=null) { + if (!workflowItemDetailsModel.getData().getWorkflowitem().getData().getStatus().equalsIgnoreCase("open")) { + item.add(new AttributeModifier("style", "pointer-events: none; opacity: 0.5;")); + } + } + }); + + + } + } try{ AmpCategorySelectFieldPanel adjustmentTypes = new AmpCategorySelectFieldPanel( "adjustmentType", CategoryConstants.ADJUSTMENT_TYPE_KEY, - new PropertyModel(model,"adjustmentType"), + new PropertyModel<>(model, "adjustmentType"), COMPONENT_FUNDING_ADJUSTMENT_TYPE, //fmname false, false, false, null, false); adjustmentTypes.getChoiceContainer().setRequired(true); @@ -73,7 +107,57 @@ protected void onPopulateItem(org.dgfoundation.amp.onepager.components.ListItem< item.add(adjustmentTypes); } catch(Exception e) { - logger.error("AmpCategoryGroupFieldPanel initialization failed"); + logger.error("Unable to add adjustment type dropdown: ",e); + } + try { + AmpTextAreaFieldPanel rejectReason = new AmpTextAreaFieldPanel("componentRejectReason", new PropertyModel<>(model, "componentRejectReason"), "Reject Reason", false, false, false); + rejectReason.setOutputMarkupId(true); +// rejectReason.setVisible(false); + if (!model.getObject().getComponentFundingStatus().getValue().equalsIgnoreCase("rejected")) { + rejectReason.add(new AttributeModifier("style", "display: none;")); + } + + item.add(rejectReason); + + AmpCategorySelectFieldPanel componentFundingStatus = new AmpCategorySelectFieldPanel( + "componentFundingStatus", CategoryConstants.COMPONENT_FUNDING_STATUS_KEY, + new PropertyModel<>(model, "componentFundingStatus"), + COMPONENT_FUNDING_STATUS, //fmname + false, false, false, null, false) + ; + componentFundingStatus.getChoiceContainer().add(new AjaxFormComponentUpdatingBehavior("onchange") { + @Override + protected void onUpdate(AjaxRequestTarget target) { + String selectedValue = model.getObject().getComponentFundingStatus().getValue(); + logger.info("Selected Status: "+selectedValue); + if ("rejected".equalsIgnoreCase(selectedValue)) { + target.appendJavaScript("$('#" + rejectReason.getMarkupId() + "').show();"); + } else { + target.appendJavaScript("$('#" + rejectReason.getMarkupId() + "').hide();"); + } + + + } + }); + componentFundingStatus.getChoiceContainer().setRequired(true); + componentFundingStatus.getChoiceContainer().add(new AttributeModifier("style", "width: 100px;")); + item.add(componentFundingStatus); + } catch (Exception e) + { + logger.info("Unable to add component funding status dropdown: ",e); + } + try { + AmpComponentFundingResourcesTableFeature resourcesList = + new AmpComponentFundingResourcesTableFeature("componentFundingDocuments", "Component Funding Documents", model); + item.add(resourcesList); + + AmpComponentFundingNewResourceFieldPanel newDoc = + new AmpComponentFundingNewResourceFieldPanel("addNewComponentFundingDocument", model, "Add New Document", resourcesList); + newDoc.setOutputMarkupId(true); + item.add(newDoc); + }catch (Exception e) + { + logger.error("Unable to add component funding documents table: ",e); } // read the list of organizations from related organizations page, and @@ -114,8 +198,8 @@ private AmpSelectFieldPanel buildSelectFieldPanel(String id, St IModel model, AbstractReadOnlyModel> orgsList) { - AmpSelectFieldPanel selectField = new AmpSelectFieldPanel(id, - new PropertyModel(model, expression), orgsList, fmName + AmpSelectFieldPanel selectField = new AmpSelectFieldPanel<>(id, + new PropertyModel<>(model, expression), orgsList, fmName , false, true, null, false); selectField.add(UpdateEventBehavior.of(FundingOrgListUpdateEvent.class)); selectField.getChoiceContainer().add(new AttributeModifier("style", "width: 100px;")); @@ -138,13 +222,10 @@ public List getObject() { List result = new ArrayList(); Set allComp = compFundsModel.getObject(); if (allComp != null){ - Iterator iterator = allComp.iterator(); - while (iterator.hasNext()) { - AmpComponentFunding comp = (AmpComponentFunding) iterator - .next(); + for (AmpComponentFunding comp : allComp) { if (comp.getTransactionType() == transactionType) //if (comp.getComponent().hashCode() == componentModel.getObject().hashCode()) - result.add(comp); + result.add(comp); } } diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpResourcesFormTableFeature.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpResourcesFormTableFeature.java index 112e8f3e990..a84af7f0305 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpResourcesFormTableFeature.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/features/tables/AmpResourcesFormTableFeature.java @@ -128,19 +128,20 @@ public List getObject() { HashSet delItems = getSession().getMetaData(OnePagerConst.RESOURCES_DELETED_ITEMS); if (delItems == null) delItems = new HashSet(); - List ret = new ArrayList(); if (refreshExistingDocs) existingTmpDocs = getExistingObject(); - ret.addAll(existingTmpDocs); + + if (existingTmpDocs==null) + { + existingTmpDocs=new ArrayList<>(); + } + List ret = new ArrayList<>(existingTmpDocs); if (am.getObject().getActivityDocuments() == null) - am.getObject().setActivityDocuments(new HashSet()); - Iterator it = setModel.getObject().iterator(); + am.getObject().setActivityDocuments(new HashSet<>()); - while (it.hasNext()) { - AmpActivityDocument d = (AmpActivityDocument) it - .next(); + for (AmpActivityDocument d : setModel.getObject()) { //check if marked for delete if (delItems.contains(d)) { for (TemporaryDocument td : existingTmpDocs) { diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/fields/AmpCategoryFieldPanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/fields/AmpCategoryFieldPanel.java index f082691c2a7..5b26de81eea 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/fields/AmpCategoryFieldPanel.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/fields/AmpCategoryFieldPanel.java @@ -98,8 +98,7 @@ public AmpCategoryFieldPanel(String id, String categKey, String fmName, public List getObject() { try { - List collectionByKey = CategoryManagerUtil.getAllAcceptableValuesForACVClass(categoryKey, relatedChoicesModel == null ? null : relatedChoicesModel.getObject()); - return collectionByKey; + return CategoryManagerUtil.getAllAcceptableValuesForACVClass(categoryKey, relatedChoicesModel == null ? null : relatedChoicesModel.getObject()); } catch (Exception e) { logger.error(e.getMessage(), e); throw new RuntimeException(e); diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/fields/AmpComponentFundingNewResourceFieldPanel.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/fields/AmpComponentFundingNewResourceFieldPanel.java new file mode 100644 index 00000000000..bc85de4d2fb --- /dev/null +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/fields/AmpComponentFundingNewResourceFieldPanel.java @@ -0,0 +1,413 @@ +package org.dgfoundation.amp.onepager.components.fields; + +import org.apache.wicket.AttributeModifier; +import org.apache.wicket.MetaDataKey; +import org.apache.wicket.Session; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.markup.html.AjaxLink; +import org.apache.wicket.behavior.AttributeAppender; +import org.apache.wicket.markup.html.WebMarkupContainer; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.markup.html.form.upload.FileUpload; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; +import org.apache.wicket.model.PropertyModel; +import org.apache.wicket.util.upload.FileItem; +import org.dgfoundation.amp.onepager.AmpAuthWebSession; +import org.dgfoundation.amp.onepager.OnePagerConst; +import org.dgfoundation.amp.onepager.OnePagerUtil; +import org.dgfoundation.amp.onepager.components.features.AmpFeaturePanel; +import org.dgfoundation.amp.onepager.components.features.tables.AmpComponentFundingResourcesTableFeature; +import org.dgfoundation.amp.onepager.components.upload.FileUploadPanel; +import org.dgfoundation.amp.onepager.helper.ResourceTranslation; +import org.dgfoundation.amp.onepager.helper.ResourceTranslationStore; +import org.dgfoundation.amp.onepager.helper.TemporaryComponentFundingDocument; +import org.dgfoundation.amp.onepager.helper.TemporaryDocument; +import org.dgfoundation.amp.onepager.models.ResourceTranslationModel; +import org.dgfoundation.amp.onepager.translation.TranslatorUtil; +import org.dgfoundation.amp.onepager.translation.TrnLabel; +import org.dgfoundation.amp.onepager.util.AmpFMTypes; +import org.dgfoundation.amp.onepager.util.FMUtil; +import org.digijava.kernel.ampapi.endpoints.filetype.FileTypeManager; +import org.digijava.kernel.ampapi.endpoints.filetype.FileTypeValidationResponse; +import org.digijava.kernel.ampapi.endpoints.filetype.FileTypeValidationStatus; +import org.digijava.kernel.translator.TranslatorWorker; +import org.digijava.module.aim.dbentity.AmpComponentFunding; +import org.digijava.module.aim.helper.GlobalSettingsConstants; +import org.digijava.module.aim.util.FeaturesUtil; +import org.digijava.module.contentrepository.util.DocumentManagerUtil; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +public class AmpComponentFundingNewResourceFieldPanel extends AmpFeaturePanel { + + + private static final long serialVersionUID = 1L; + + protected WebMarkupContainer webLinkFeedbackContainer; + protected Label webLinkFeedbackLabel; + protected FileUploadPanel fileUpload; + boolean resourceIsURL = false; + protected boolean pathSelected; + protected boolean urlSelected; + protected boolean contentValid; + + final protected String DEFAULT_MESSAGE = "*" + TranslatorUtil.getTranslatedText("Please enter title"); + final protected String URL_NOT_SELECTED = "*" + TranslatorUtil.getTranslatedText("URL not selected"); + final protected String FILE_PATH_NOT_SELECTED = "*" + TranslatorUtil.getTranslatedText("File not submitted or upload has not finished"); + final protected String CONTENT_TYPE_NOT_ALLOWED = "*" + TranslatorUtil.getTranslatedText("Content type not allowed:"); + final protected String CONTENT_TYPE_EXTENSION_MISMATCH = "*" + TranslatorUtil.getTranslatedText("File extension does not match the actual file format:"); + final protected String CONTENT_TYPE_INTERNAL_ERROR = "*" + TranslatorUtil.getTranslatedText("Internal error during the content validation"); + + boolean webLinkFormatCorrect; + protected Model newResourceIdModel = new Model<>(); + + public AmpComponentFundingNewResourceFieldPanel(final String id, + final IModel model, + final String fmName, + final AmpComponentFundingResourcesTableFeature resourcesList) { + + super(id, model, fmName, true); + + TemporaryComponentFundingDocument tmpDoc = new TemporaryComponentFundingDocument (); + String docId =generateResourceKey("newResource"); + newResourceIdModel.setObject(docId); + tmpDoc.setNewTemporaryDocumentId(docId); + final IModel td = new Model<>(tmpDoc); + final ResourceTranslationModel titleModel = new ResourceTranslationModel(new PropertyModel(td, "title"),newResourceIdModel); + final AmpTextFieldPanel name = new AmpTextFieldPanel<>("componentFundingDocumentDocTitle", titleModel, "Title", AmpFMTypes.MODULE, Boolean.TRUE); + name.setTextContainerDefaultMaxSize(); + name.setOutputMarkupId(true); + final ResourceTranslationModel descModel = new ResourceTranslationModel(new PropertyModel(td, "description"),newResourceIdModel); + final AmpTextAreaFieldPanel desc = new AmpTextAreaFieldPanel("componentFundingDocumentDocDesc", descModel, "Description", false, false, false); + desc.setOutputMarkupId(true); + final ResourceTranslationModel noteModel = new ResourceTranslationModel(new PropertyModel(td, "note"),newResourceIdModel); + final AmpTextAreaFieldPanel note = new AmpTextAreaFieldPanel("componentFundingDocumentDocNote",noteModel, "Note", false, false, false); + note.setOutputMarkupId(true); +// final AmpCategorySelectFieldPanel type = new AmpCategorySelectFieldPanel("docType", CategoryConstants.DOCUMENT_TYPE_KEY, new PropertyModel(td, "type"), "Type", true, true); + //FileUploadField file = new FileUploadField("file", new AmpFileUploadModel(new PropertyModel(td, "file"))); + //file.setOutputMarkupId(true); + + String componentId = "new"; + if (model.getObject().getAmpComponentFundingId() != null) + componentId = Long.toString(model.getObject().getAmpComponentFundingId()); + final Model fileItemModel = new Model(); + fileUpload = new FileUploadPanel("componentFundingDocumentFile",componentId, fileItemModel); + fileUpload.setOutputMarkupId(true); + +// final AmpTextFieldPanel webLink = new AmpTextFieldPanel("webLink", +// new PropertyModel(td, "webLink"), "Web Link", true, true); +// webLink.setTextContainerDefaultMaxSize(); +// webLink.setVisibilityAllowed(false); +// webLink.setOutputMarkupId(true); + + String resourceLabelModel = "File"; +// if (newResourceIsWebLink) { +// resourceLabelModel = "Web Link"; +// resourceIsURL = true; +// } + + TrnLabel resourceLabel = new TrnLabel("componentFundingDocumentResourceLabel", resourceLabelModel); + + // create the form + final Form form = new Form("componentFundingDocumentForm") { + + /** + * @see org.apache.wicket.markup.html.form.Form#onSubmit() + */ + @Override + protected void onSubmit() { + TemporaryComponentFundingDocument tmp = td.getObject(); + + if (fileItemModel.getObject() != null) + tmp.setFile(new FileUpload(fileItemModel.getObject())); + + if (updateVisibility(td.getObject(), resourceIsURL)) { + if (tmp.getFile() != null){ + double fSize = tmp.getFile().getSize()*100/(1024*1024); + fSize = fSize/100; + tmp.setFileSize(fSize); + tmp.setFileName(tmp.getFile().getClientFileName()); + tmp.setContentType(tmp.getFile().getContentType()); + } + + if (tmp.getWebLink() != null) { + tmp.setWebLink(DocumentManagerUtil.processUrl(tmp.getWebLink(), null)); + tmp.setFileName(tmp.getWebLink()); + } + + tmp.setDate(Calendar.getInstance()); + tmp.setYear(String.valueOf((tmp.getDate()).get(Calendar.YEAR))); +// HashSet newItemsSet = getSession().getMetaData(OnePagerConst.COMPONENT_FUNDING_NEW_ITEMS).get(model.getObject().getJustAnId()); +// if (newItemsSet == null) { +// newItemsSet = new HashSet<>(); +//// getSession().getMetaData(OnePagerConst.COMPONENT_FUNDING_NEW_ITEMS).get(model.getObject().getJustAnId()).addAll(newItemsSet); +// } + + MetaDataKey>> metaDataKey = OnePagerConst.COMPONENT_FUNDING_NEW_ITEMS; + + HashMap> metaData = getSession().getMetaData(metaDataKey); + + if (metaData == null) { + metaData = new HashMap<>(); + } + getSession().setMetaData(metaDataKey, metaData); + + HashSet newItemsSet = metaData.computeIfAbsent(model.getObject().getJustAnId(), k -> new HashSet<>()); + + +// existingSet.addAll(newItemsSet); + + tmp.setTranslatedDescriptionList(getTranslationsForField(tmp.getNewTemporaryDocumentId(),"description")); + tmp.setTranslatedTitleList(getTranslationsForField(tmp.getNewTemporaryDocumentId(),"title")); + tmp.setTranslatedNoteList(getTranslationsForField(tmp.getNewTemporaryDocumentId(),"description")); + newItemsSet.add(tmp); + TemporaryComponentFundingDocument tmpDoc = new TemporaryComponentFundingDocument(); + String docId = generateResourceKey("newResource"); + newResourceIdModel.setObject(docId); + tmpDoc.setNewTemporaryDocumentId(docId); + td.setObject(tmpDoc); + fileItemModel.setObject(null); + } + } + }; + + add(createAddNewLink(fmName)); + + WebMarkupContainer rc = new WebMarkupContainer("componentFundingDocumentResourcePanel"); + rc.add(new AttributeModifier("id", getToggleId())); + rc.add(form); + rc.add(name); + rc.add(fileUpload); + rc.setOutputMarkupId(true); + add(rc); + +// if (newResourceIsWebLink){ +// fileUpload.setVisible(false); +// webLink.setVisibilityAllowed(true); +// } + + form.add(name); + form.add(desc); + form.add(note); +// form.add(type); + form.add(fileUpload); + form.add(resourceLabel); +// form.add(webLink); + + // create the ajax button used to submit the form + AmpButtonField submit = new AmpButtonField("componentFundingDocumentAjaxSubmit", "Add", true){ + @Override + protected void onSubmit(AjaxRequestTarget target, Form form) { + TemporaryDocument tmp = td.getObject(); + if (fileItemModel.getObject() != null) + tmp.setFile(new FileUpload(fileItemModel.getObject())); + target.add(name); + target.add(desc); + target.add(note); +// target.add(type); + +// if (webLink.isVisibleInHierarchy()) { +// target.add(webLink); +// } + + target.add(resourcesList); +// target.add(webLinkFeedbackContainer); + if (updateVisibility(td.getObject(), resourceIsURL)){ + target.appendJavaScript("$('#" + getToggleId() + "').hide();"); + target.appendJavaScript("$('#" + getToggleId() + "').find('[role=fileUploadedMsg]').html('');"); + target.appendJavaScript("$('#uploadLabel').text('" + TranslatorWorker.translateText("No file chosen") + "');"); + + } + } + }; + + form.add(submit); + form.add(createCancelButton()); + + createWebLinkFeedbackContainer(); + form.add(webLinkFeedbackContainer); + } + + @Override + protected void onConfigure() { + super.onConfigure(); + // Make file upload respect FM enabled state + if (fileUpload != null) { + boolean fmMode = ((AmpAuthWebSession) getSession()).isFmMode(); + if (fmMode) { + // In FM mode, check if this panel is enabled + boolean parentEnabled = FMUtil.isFmEnabled(this); + fileUpload.setVisible(parentEnabled); + } else { + // In normal mode, respect parent's visibility + fileUpload.setVisible(isVisibleInHierarchy()); + } + } + } + + protected AjaxLink createAddNewLink(final String fmName) { + final String newDocumentGenKey = TranslatorWorker.generateTrnKey(fmName); + final AjaxLink addNewLink = new AjaxLink("componentFundingDocumentPanelLink"){ + public void onClick(AjaxRequestTarget target) { +// target.prependJavaScript(OnePagerUtil.getToggleChildrenJS(this)); + target.prependJavaScript(OnePagerUtil.getToggleChildrenJSComponentFunding(this, getToggleId())); + } + + @Override + protected void onConfigure() { + super.onConfigure(); + configureTranslationMode(this, newDocumentGenKey); + } + }; + addNewLink.add(new Label("componentFundingDocumentLinkText", TranslatorWorker.translateText(fmName))); + + return addNewLink; + } + + protected AmpAjaxLinkField createCancelButton() { + + return new AmpAjaxLinkField("componentFundingDocumentCancel", "Cancel", "Cancel") { + @Override + protected void onClick(AjaxRequestTarget target) { + target.appendJavaScript("$('#" + getToggleId() + "').hide();"); + target.appendJavaScript("$('#" + getToggleId() + "').find('[role=fileUploadedMsg]').html('');"); + target.appendJavaScript("$('#uploadLabel').text('" + TranslatorWorker.translateText("No file chosen") + "');"); +// webLinkFeedbackContainer.setVisible(false); + } + }; + } + + protected void createWebLinkFeedbackContainer() { + webLinkFeedbackContainer = new WebMarkupContainer("webLinkFeedbackContainer"); + webLinkFeedbackContainer.setOutputMarkupId(true); + webLinkFeedbackContainer.setOutputMarkupPlaceholderTag(true); + webLinkFeedbackContainer.setVisible(false); + + webLinkFeedbackLabel = new Label("webLinkFeedbackLabel", new Model(DEFAULT_MESSAGE)); + webLinkFeedbackContainer.add(webLinkFeedbackLabel); + } + + protected boolean updateVisibility(TemporaryDocument resource, boolean newResourceIsWebLink) { + boolean noErrors = true; + boolean titleSelected = !(resource.getTitle() == null || resource.getTitle().length() == 0); + + String conentValidationMessage = ""; + + if (newResourceIsWebLink){ + urlSelected = resource.getWebLink() != null && resource.getWebLink().length() != 0; + } else { + pathSelected = resource.getFile() != null; + contentValid = true; + if (pathSelected) { + // validate the content of the file AMP-24920 + if (isEnabledMimeTypeValidation()) { + try { + FileTypeManager mimeTypeManager = FileTypeManager.getInstance(); + InputStream is = new BufferedInputStream(resource.getFile().getInputStream()); + FileTypeValidationResponse validationResponse = mimeTypeManager.validateFileType(is, resource.getFile().getClientFileName()); + if (validationResponse.getStatus() != FileTypeValidationStatus.ALLOWED) { + if (validationResponse.getStatus() == FileTypeValidationStatus.NOT_ALLOWED) { + conentValidationMessage = CONTENT_TYPE_NOT_ALLOWED + " " + + validationResponse.getDescription(); + + } else if (validationResponse.getStatus() == FileTypeValidationStatus.CONTENT_EXTENSION_MISMATCH) { + conentValidationMessage = CONTENT_TYPE_EXTENSION_MISMATCH + " " + + resource.getFile().getClientFileName() + " " + + TranslatorUtil.getTranslatedText("is a ") + " " + + validationResponse.getDescription() + " (" + + validationResponse.getContentName() + ")"; + } else { + conentValidationMessage = CONTENT_TYPE_INTERNAL_ERROR; + } + contentValid = false; + } + } catch (IOException e) { + conentValidationMessage = CONTENT_TYPE_INTERNAL_ERROR; + contentValid = false; + } + } + } + } + + if (!titleSelected && !((pathSelected && contentValid))) { + noErrors = false; + if (!titleSelected) { + webLinkFeedbackLabel.setDefaultModelObject(DEFAULT_MESSAGE); + } else if (!pathSelected && !newResourceIsWebLink) { + webLinkFeedbackLabel.setDefaultModelObject(FILE_PATH_NOT_SELECTED); + } else if (!contentValid && !newResourceIsWebLink) { + webLinkFeedbackLabel.setDefaultModelObject(conentValidationMessage); + } else if (!urlSelected && newResourceIsWebLink) { + webLinkFeedbackLabel.setDefaultModelObject(URL_NOT_SELECTED); + } + } + + return noErrors; + } + + private boolean isEnabledMimeTypeValidation() { + return FeaturesUtil.getGlobalSettingValueBoolean(GlobalSettingsConstants.LIMIT_FILE_TYPE_FOR_UPLOAD); + } + + protected String generateResourceKey(String id) { + AmpAuthWebSession session = ((AmpAuthWebSession) Session.get()); + String eKey = id + "-" + session.getCurrentMember().getMemberId() + "-"; + eKey = eKey + System.currentTimeMillis(); + + return eKey; + } + + private List getTranslationsForField (String id, String field) { + List translationsList; + HashMap translationMap = Session.get().getMetaData( + OnePagerConst.RESOURCES_TRANSLATIONS); + if (translationMap != null) { + ResourceTranslationStore store = translationMap.get(id); + if (store != null) { + translationsList = store.getResourceFieldTranslations().get(field); + return translationsList; + } + } + return null; + } + + protected void configureTranslationMode(AjaxLink link, String key) { + if (TranslatorUtil.isTranslatorMode(getSession())){ + link.setOutputMarkupId(true); + link.add(new AttributeAppender("style", new Model("text-decoration: underline; color: #0CAD0C;"), "")); + link.add(new AttributeModifier("key", key)); +// link.add(new AttributeModifier("onclick", "$('#" + getToggleId() + "').slideToggle();spawnEditBox(this.id)")); + link.add(new AttributeModifier("onclick", String.format("$(this).closest('tr.wicketFundingRowItem').find('#%s').slideToggle();spawnEditBox(this.id)",getToggleId()))); +// link.add(new AttributeModifier("onclick", "toggleElement(this, '" + getToggleId() + "');spawnEditBox(this.id)")); +// link.add(new AttributeModifier("onclick", "var row = $(this).closest('tr'); var element = row.find('#" + getToggleId() + "'); element.slideToggle(); return false;spawnEditBox(this.id)")); + + } else { + link.add(AttributeModifier.remove("key")); + link.add(AttributeModifier.remove("style")); + link.add(AttributeModifier.remove("onclick")); +// link.add(new AttributeModifier("onclick", "$('#" + getToggleId() + "').slideToggle();")); + link.add(new AttributeModifier("onclick", String.format("$(this).closest('tr.wicketFundingRowItem').find('#%s').slideToggle();",getToggleId()))); +// link.add(new AttributeModifier("onclick", "console.log('Link clicked'); var row = $(this).closest('tr'); var element = row.find('#" + getToggleId() + "'); console.log(element); element.slideToggle(); return false;")); + +// link.add(new AttributeModifier("onclick", "toggleElement(this, '" + getToggleId() + "');")); + + } + } + + protected String getToggleId() { + if (TranslatorUtil.isTranslatorMode(getSession())) { + return getTranlationToggleId(); + } + + return "id_" + getId(); + } + + protected String getTranlationToggleId() { + return getId() + "H"; + } +} \ No newline at end of file diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/upload/FileUploadBehavior.java b/amp/src/main/java/org/dgfoundation/amp/onepager/components/upload/FileUploadBehavior.java index a343c4a9c63..450ffdde237 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/upload/FileUploadBehavior.java +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/upload/FileUploadBehavior.java @@ -79,6 +79,7 @@ public void renderHead(final Component component, IHeaderResponse response) { variables.put("uploadFailedMsg", TranslatorUtil.getTranslatedText("Upload failed! Please try again.")); variables.put("uploadStartedMsg", TranslatorUtil.getTranslatedText("Upload started, please wait...")); variables.put("uploadFailedTooBigMsg", TranslatorUtil.getTranslatedText("The file size limit is {size} MB. This file exceeds the limit.").replace("{size}", maxFileSizeGS)); + variables.put("uploadFailedTooSmallMsg", TranslatorUtil.getTranslatedText("This file is empty.Please upload a non-empty file.")); variables.put("uploadMaxFileSize", Long.toString(Bytes.megabytes(Long.parseLong(maxFileSizeGS)).bytes())); variables.put("uploadNoFileLabel", TranslatorWorker.translateText("No file chosen")); diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/components/upload/FileUploadBehavior.js b/amp/src/main/java/org/dgfoundation/amp/onepager/components/upload/FileUploadBehavior.js index 70d07f3caaa..301be3c83fc 100644 --- a/amp/src/main/java/org/dgfoundation/amp/onepager/components/upload/FileUploadBehavior.js +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/components/upload/FileUploadBehavior.js @@ -26,7 +26,19 @@ function setupFileUpload(componentId, componentUrl, componentParamName){ $('#uploadLabel').text("${uploadNoFileLabel}"); $(this).find('[role=fileUploadedMsg]').html(''); $(this).find('[role=fileUploadedMsg]').hide(); - } else { + } + else if (data.files[0].size <= 0) { + alert("${uploadFailedTooSmallMsg}"); + $('#uploadLabel').text("${uploadNoFileLabel}"); + $(this).find('[role=fileUploadedMsg]').html(''); + $(this).find('[role=fileUploadedMsg]').hide(); + } else if (data.title === '') { + alert("Document Title must be present."); + $('#uploadLabel').text("${uploadNoFileLabel}"); + $(this).find('[role=fileUploadedMsg]').html(''); + $(this).find('[role=fileUploadedMsg]').hide(); + } + else { $(this).find('[role=fileUploadedMsg]').show(); $(this).find('[role=fileUploadedMsg]').html(" \"" + "${uploadStartedMsg}" + data.files[0].size + "\" bytes"); data.submit(); diff --git a/amp/src/main/java/org/dgfoundation/amp/onepager/helper/TemporaryComponentFundingDocument.java b/amp/src/main/java/org/dgfoundation/amp/onepager/helper/TemporaryComponentFundingDocument.java new file mode 100644 index 00000000000..bd8d418724f --- /dev/null +++ b/amp/src/main/java/org/dgfoundation/amp/onepager/helper/TemporaryComponentFundingDocument.java @@ -0,0 +1,76 @@ +package org.dgfoundation.amp.onepager.helper; + +import org.digijava.module.aim.dbentity.AmpComponentFundingDocument; +import org.digijava.module.contentrepository.jcrentity.Label; + +import java.util.List; + +public class TemporaryComponentFundingDocument extends TemporaryDocument{ + private String description; + private String note; + private String year; + + private List