← Back to team overview

dhis2-devs team mailing list archive

[Branch ~dhis2-devs-core/dhis2/trunk] Rev 15578: Allow data approvals for composite periods spanning multiple data set periods.

 

------------------------------------------------------------
revno: 15578
committer: jimgrace@xxxxxxxxx
branch nick: dhis2
timestamp: Fri 2014-06-06 15:29:39 -0400
message:
  Allow data approvals for composite periods spanning multiple data set periods.
added:
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DataApprovalPeriodAggregator.java
modified:
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/MapMap.java
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApproval.java
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalBaseState.java
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalState.java
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DataApprovalSelection.java
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DefaultDataApprovalService.java
  dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataapproval/DataApprovalServiceTest.java
  dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/util/CollectionUtils.java
  dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataApprovalController.java
  dhis-2/dhis-web/dhis-web-reporting/src/main/java/org/hisp/dhis/reporting/dataapproval/action/GetDataApprovalOptionsAction.java
  dhis-2/dhis-web/dhis-web-reporting/src/main/resources/org/hisp/dhis/reporting/i18n_module.properties
  dhis-2/dhis-web/dhis-web-reporting/src/main/webapp/dhis-web-reporting/dataApprovalForm.vm
  dhis-2/dhis-web/dhis-web-reporting/src/main/webapp/dhis-web-reporting/javascript/dataApproval.js


--
lp:dhis2
https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk

Your team DHIS 2 developers is subscribed to branch lp:dhis2.
To unsubscribe from this branch go to https://code.launchpad.net/~dhis2-devs-core/dhis2/trunk/+edit-subscription
=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/MapMap.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/MapMap.java	2014-05-18 00:49:40 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/MapMap.java	2014-06-06 19:29:39 +0000
@@ -28,6 +28,7 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import java.util.AbstractMap;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -57,4 +58,16 @@
     {
         return this.get( key ) == null ? null : this.get( key ).get( valueKey );
     }
+
+    public static <T, U, V> MapMap<T, U, V> asMapMap( AbstractMap.SimpleEntry<T, Map<U, V>>... entries )
+    {
+        MapMap<T, U, V> map = new MapMap<T, U, V>();
+
+        for ( AbstractMap.SimpleEntry<T, Map<U, V>> entry : entries )
+        {
+            map.put( entry.getKey(), entry.getValue() );
+        }
+
+        return map;
+    }
 }

=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApproval.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApproval.java	2014-04-07 04:27:17 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApproval.java	2014-06-06 19:29:39 +0000
@@ -122,6 +122,18 @@
         this.creator = creator;
     }
 
+    public DataApproval( DataApproval da )
+    {
+        this.dataApprovalLevel = da.dataApprovalLevel;
+        this.dataSet = da.dataSet;
+        this.period = da.period;
+        this.organisationUnit = da.organisationUnit;
+        this.categoryOptionGroup = da.categoryOptionGroup;
+        this.accepted = da.accepted;
+        this.created = da.created;
+        this.creator = da.creator;
+    }
+
     // -------------------------------------------------------------------------
     // Getters and setters
     // -------------------------------------------------------------------------

=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalBaseState.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalBaseState.java	2014-04-14 06:52:39 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalBaseState.java	2014-06-06 19:29:39 +0000
@@ -52,11 +52,23 @@
     UNAPPROVED_READY,
 
     /**
+     * Data is approved for some but not all periods inside this longer period
+     * and is ready for approval in all periods inside this containing period.
+     */
+    PARTIALLY_APPROVED,
+
+    /**
      * Data is approved (either here or elsewhere).
      */
     APPROVED,
 
     /**
+     * Data is accepted for some but not all periods inside this longer period
+     * and is ready for accepting in all periods inside this containing period.
+     */
+    PARTIALLY_ACCEPTED,
+
+    /**
      * Data is approved and accepted (either here or elsewhere).
      */
     ACCEPTED;

=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalState.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalState.java	2014-04-14 06:52:39 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalState.java	2014-06-06 19:29:39 +0000
@@ -59,17 +59,28 @@
     UNAPPROVED_READY ( DataApprovalBaseState.UNAPPROVED_READY, false, true, true, false, true ),
 
     /**
+     * Some periods within this multi-period selection are approved here
+     * and some are not approved (but ready for approval here.)
+     */
+    PARTIALLY_APPROVED_HERE( DataApprovalBaseState.PARTIALLY_APPROVED, false, false, true, false, true ),
+
+    /**
      * Data is approved, and was approved here (so could be unapproved here.)
      */
     APPROVED_HERE ( DataApprovalBaseState.APPROVED, true, false, true, false, false ),
 
     /**
+     * Some periods within this multi-period selection are approved elsewhere
+     * and some are not approved elsewhere (not approvable here.)
+     */
+    PARTIALLY_APPROVED_ELSEWHERE ( DataApprovalBaseState.PARTIALLY_APPROVED, false, false, false, false, false ),
+
+    /**
      * Data is approved, but was not approved here (so cannot be unapproved here.)
      * This covers the following cases:
      * <ul>
      * <li>Data is approved at a higher level.</li>
      * <li>Data is approved for wider scope of category options.</li>
-     * <li>Data is approved for all sub-periods in selected period.</li>
      * </ul>
      * In the first two cases, there is a single data approval object
      * that covers the selection. In the third case there is not.
@@ -77,11 +88,23 @@
     APPROVED_ELSEWHERE( DataApprovalBaseState.APPROVED, true, false, false, false, false ),
 
     /**
+     * Some periods within this multi-period selection are accepted here
+     * and some are not approved elsewhere (not approvable here.)
+     */
+    PARTIALLY_ACCEPTED_HERE( DataApprovalBaseState.PARTIALLY_ACCEPTED, true, false, true, false, false ),
+
+    /**
      * Data is approved and accepted here (so could be unapproved here.)
      */
     ACCEPTED_HERE ( DataApprovalBaseState.ACCEPTED, true, false, true, true, false ),
 
     /**
+     * Some periods within this multi-period selection are accepted elsewhere
+     * and some are approved elsewhere (not approvable here.)
+     */
+    PARTIALLY_ACCEPTED_ELSEWHERE ( DataApprovalBaseState.PARTIALLY_APPROVED, false, false, false, false, false ),
+
+    /**
      * Data is approved and accepted, but elsewhere.
      */
     ACCEPTED_ELSEWHERE ( DataApprovalBaseState.ACCEPTED, true, false, false, true, false );

=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DataApprovalPeriodAggregator.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DataApprovalPeriodAggregator.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DataApprovalPeriodAggregator.java	2014-06-06 19:29:39 +0000
@@ -0,0 +1,223 @@
+package org.hisp.dhis.dataapproval;
+
+/*
+ * Copyright (c) 2004-2014, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * Neither the name of the HISP project nor the names of its contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import org.hisp.dhis.common.MapMap;
+
+import static org.hisp.dhis.common.MapMap.*;
+import static org.hisp.dhis.dataapproval.DataApprovalState.*;
+import static org.hisp.dhis.system.util.CollectionUtils.*;
+
+/**
+ * This package-private class is used by the data approval service to
+ * form a composite data approval state for a period spanning more than
+ * one data approval period.
+ *
+ * @author Jim Grace
+ * @version $Id$
+ */
+class DataApprovalPeriodAggregator
+{
+    /**
+     * Represents the data approval state transitions from a current state
+     * representing the combined state of all periods tested so far, combined
+     * with the state of a new period, resulting in the next current state.
+     * <p>
+     * When checking states for a number of periods where all other selection
+     * criteria are the same, the states will always be in one of three
+     * mutually exclusive categories:
+     * <ul>
+     * <li>UNAPPROVABLE</li>
+     * <li>Approvable elsewhere states:
+     *     UNAPPROVED_ELSEWHERE,
+     *     PARTIALLY_APPROVED_ELSEWHERE,
+     *     APPROVED_ELSEWHERE,
+     *     ACCEPTED_ELSEWHERE,
+     *     PARTIALLY_ACCEPTED_ELSEWHERE
+     * </li>
+     * <li>Approvable here states:
+     *     UNAPPROVED_WAITING,
+     *     UNAPPROVED_READY,
+     *     PARTIALLY_APPROVED_HERE,
+     *     APPROVED_HERE,
+     *     PARTIALLY_ACCEPTED_HERE,
+     *     ACCEPTED_HERE
+     * </li>
+     * </ul>
+     * We don't have to worry about state transitions between these
+     * categories; they will not exist. We need only consider state
+     * transitions within each of the categories. (And we don't have to
+     * consider state transitions within the first category since there
+     * is only one state.)
+     * <p>
+     * The state transitions are coded in a MapMap. Conceptually, they
+     * form a triangular matrix (minus the diagonal) like the following,
+     * where "*" shows the entries:
+     * <pre>
+     *
+     * current A  B  C  D  E
+     *  new A  .  *  *  *  *
+     *      B  .  .  *  *  *
+     *      C  .  .  .  *  *
+     *      D  .  .  .  .  *
+     *
+     * </pre>
+     * The diagonal is not required because when the current and new states
+     * are the same, the next current state will be the same as both.
+     * The lower triangle of the matrix is not required because the
+     * matrix is tested both ways: current (columns) - new (rows), and also
+     * current (rows) - new (columns) (The matrix dimensions are commutative.)
+     */
+    static private final MapMap<DataApprovalState, DataApprovalState, DataApprovalState> transitionMap = asMapMap(
+
+        // ---------------------------------------------------------------------
+        // States where data can be approved, but not here
+        // ---------------------------------------------------------------------
+
+        //
+        // Data is unapproved, and is waiting for approval somewhere else.
+        //
+        asEntry( UNAPPROVED_ELSEWHERE, asMap(
+                asEntry( PARTIALLY_APPROVED_ELSEWHERE, PARTIALLY_APPROVED_ELSEWHERE ),
+                asEntry( APPROVED_ELSEWHERE, PARTIALLY_APPROVED_ELSEWHERE ),
+                asEntry( PARTIALLY_ACCEPTED_ELSEWHERE, PARTIALLY_APPROVED_ELSEWHERE ),
+                asEntry( ACCEPTED_ELSEWHERE, PARTIALLY_APPROVED_ELSEWHERE ) ) ),
+
+        //
+        // Some periods within this selection are approved elsewhere and
+        // some are unapproved elsewhere.
+        //
+        asEntry( PARTIALLY_APPROVED_ELSEWHERE, asMap(
+                asEntry( APPROVED_ELSEWHERE, PARTIALLY_APPROVED_ELSEWHERE ),
+                asEntry( PARTIALLY_ACCEPTED_ELSEWHERE, PARTIALLY_APPROVED_ELSEWHERE ),
+                asEntry( ACCEPTED_ELSEWHERE, PARTIALLY_APPROVED_ELSEWHERE ) ) ),
+
+        //
+        // Data is unapproved, and is waiting for approval somewhere else.
+        //
+        asEntry( APPROVED_ELSEWHERE, asMap(
+                asEntry( PARTIALLY_ACCEPTED_ELSEWHERE, PARTIALLY_ACCEPTED_ELSEWHERE ),
+                asEntry( ACCEPTED_ELSEWHERE, PARTIALLY_ACCEPTED_ELSEWHERE ) ) ),
+
+        //
+        // Data is approved somewhere else.
+        //
+        asEntry( PARTIALLY_ACCEPTED_ELSEWHERE, asMap(
+                asEntry( ACCEPTED_ELSEWHERE, PARTIALLY_ACCEPTED_ELSEWHERE ) ) ),
+
+        // ---------------------------------------------------------------------
+        // States where data can be approved here
+        // ---------------------------------------------------------------------
+
+        //
+        // Data is unapproved, and is waiting for some lower-level approval.
+        //
+        asEntry( UNAPPROVED_WAITING, asMap(
+                asEntry( UNAPPROVED_READY, UNAPPROVED_WAITING ),
+                asEntry( PARTIALLY_APPROVED_HERE, UNAPPROVED_WAITING ),
+                asEntry( APPROVED_HERE, UNAPPROVED_WAITING ),
+                asEntry( PARTIALLY_ACCEPTED_HERE, UNAPPROVED_WAITING ),
+                asEntry( ACCEPTED_HERE, UNAPPROVED_WAITING ) ) ),
+
+        //
+        // Data is unapproved, and is ready to be approved here.
+        //
+        asEntry( UNAPPROVED_READY, asMap(
+                asEntry( PARTIALLY_APPROVED_HERE, PARTIALLY_APPROVED_HERE ),
+                asEntry( APPROVED_HERE, PARTIALLY_APPROVED_HERE ),
+                asEntry( PARTIALLY_ACCEPTED_HERE, PARTIALLY_APPROVED_HERE ),
+                asEntry( ACCEPTED_HERE, PARTIALLY_APPROVED_HERE ) ) ),
+
+        //
+        // Data is approved for some but not all periods inside this longer period
+        // and is ready for approval in all periods inside this containing period.
+        //
+        asEntry( PARTIALLY_APPROVED_HERE, asMap(
+                asEntry( APPROVED_HERE, PARTIALLY_APPROVED_HERE ),
+                asEntry( PARTIALLY_ACCEPTED_HERE, PARTIALLY_APPROVED_HERE ),
+                asEntry( ACCEPTED_HERE, PARTIALLY_APPROVED_HERE ) ) ),
+
+        //
+        // Data is approved, and was approved here.
+        //
+        asEntry( APPROVED_HERE, asMap(
+                asEntry( PARTIALLY_ACCEPTED_HERE, PARTIALLY_ACCEPTED_HERE ),
+                asEntry( ACCEPTED_HERE, PARTIALLY_ACCEPTED_HERE ) ) ),
+
+        //
+        // Data is accepted for some but not all periods inside this longer period
+        // and is ready to be accepted in all periods inside this containing period.
+        //
+        asEntry( PARTIALLY_ACCEPTED_HERE, asMap(
+                asEntry( ACCEPTED_HERE, PARTIALLY_ACCEPTED_HERE ) ) )
+    );
+
+    /**
+     * Finds the next data approval state for the multi-period selection by
+     * considering the current aggregate state of all periods so far, and the
+     * state of a new, additional period.
+     * <p>
+     * Note that that arguments to this function have the commutative property.
+     * It is unimportant as to which is the current composite state and
+     * which is the new state for a period within the data selection.
+     *
+     * @param s1 current aggregate state (or new state)
+     * @param s2 new period state (or current state)
+     * @return the next current state
+     */
+    static DataApprovalState nextState( DataApprovalState s1, DataApprovalState s2 )
+    {
+        return firstNonNull(
+            transitionMap.getValue( s1, s2 ),
+            transitionMap.getValue( s2, s1 ),
+            s1,
+            s2 );
+    }
+
+    /**
+     * Returns the first non-null argument. This simulates a method found in
+     * org.apache.commons.lang3.ObjectUtils, and can be replaced some day
+     * by that or a comparable method.
+     *
+     * @param values values to check
+     * @param <T> type of items
+     * @return the first non-null item
+     */
+    private static <T> T firstNonNull(T ...values)
+    {
+        for ( T value : values )
+        {
+            if ( value != null )
+            {
+                return value;
+            }
+        }
+        return null;
+    }
+}

=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DataApprovalSelection.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DataApprovalSelection.java	2014-05-04 21:36:18 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DataApprovalSelection.java	2014-06-06 19:29:39 +0000
@@ -59,6 +59,7 @@
  * between methods.
  *
  * @author Jim Grace
+ * @version $Id$
  */
 class DataApprovalSelection
 {
@@ -189,7 +190,7 @@
         {
             if ( period.getPeriodType().getFrequencyOrder() > dataSet.getPeriodType().getFrequencyOrder() )
             {
-                findStatusForLongerPeriodType();
+                findStatusForCompositePeriod();
             }
             else
             {
@@ -250,19 +251,8 @@
      * Handles the case where the selected period type is longer than the
      * data set period type. The selected period is broken down into data
      * set type periods. The approval status of the selected period is
-     * constructed by logic that combines the approval statuses of the
-     * constituent periods.
-     * <p>
-     * If the data is unapproved for any time segment, returns
-     * UNAPPROVED_ELSEWHERE.
-     * <p>
-     * If the data is accepted for all time segments, returns
-     * ACCEPTED_ELSEWHERE.
-     * <p>
-     * If the data is approved for all time segments (and maybe accepted for
-     * some but not all), returns APPROVED_ELSEWHERE.
-     * <p>
-     * Note that the dataApproval object always returns null.
+     * constructed by state transition logic that combines the approval
+     * statuses of the constituent periods.
      * <p>
      * If data is accepted and/or approved in all time periods, the
      * dataApprovalLevel object reference points to the lowest level of
@@ -272,8 +262,10 @@
      *
      * @return status status of the longer period
      */
-    private void findStatusForLongerPeriodType()
+    private void findStatusForCompositePeriod()
     {
+        Period longerPeriod = period;
+
         Collection<Period> testPeriods = periodService.getPeriodsBetweenDates( dataSet.getPeriodType(), period.getStartDate(), period.getEndDate() );
 
         DataApprovalLevel lowestApprovalLevel = null;
@@ -282,65 +274,44 @@
         {
             period = testPeriod;
 
-            DataApprovalState s = getState();
+            state = DataApprovalPeriodAggregator.nextState( state, getState() );
 
-            switch ( s )
+            switch ( state )
             {
+                case PARTIALLY_APPROVED_HERE:
                 case APPROVED_HERE:
                 case APPROVED_ELSEWHERE:
-
-                    state = DataApprovalState.APPROVED_ELSEWHERE;
-
-                    dataApproval = null;
-
-                    if ( lowestApprovalLevel == null || dataApprovalLevel.getLevel() > lowestApprovalLevel.getLevel() )
-                    {
-                        lowestApprovalLevel = dataApprovalLevel;
-                    }
-
-                    break;
-
+                case PARTIALLY_ACCEPTED_HERE:
                 case ACCEPTED_HERE:
                 case ACCEPTED_ELSEWHERE:
-
-                    if ( state == null )
-                    {
-                        state = DataApprovalState.ACCEPTED_ELSEWHERE;
-                    }
-
-                    dataApproval = null;
-
-                    if ( lowestApprovalLevel == null || dataApprovalLevel.getLevel() > lowestApprovalLevel.getLevel() )
-                    {
-                        lowestApprovalLevel = dataApprovalLevel;
-                    }
-
-                    break;
-
                 case UNAPPROVED_READY:
+
+                    if ( lowestApprovalLevel == null || ( dataApprovalLevel != null
+                            && dataApprovalLevel.getLevel() > lowestApprovalLevel.getLevel() ) )
+                    {
+                        lowestApprovalLevel = dataApprovalLevel;
+                    }
+
+                    break;
+
                 case UNAPPROVED_WAITING:
                 case UNAPPROVED_ELSEWHERE:
-
-                    dataApproval = null;
-                    dataApprovalLevel = null;
-
-                    state = DataApprovalState.UNAPPROVED_ELSEWHERE;
-
-                    return;
-
                 case UNAPPROVABLE:
                 default: // (Not expected)
 
-                    state = s;
-
                     dataApproval = null;
                     dataApprovalLevel = null;
 
-                    return;
+                    return; // No further state transitions are possible from these three states.
             }
         }
 
         dataApprovalLevel = lowestApprovalLevel;
+        if ( dataApproval != null )
+        {
+            dataApproval = new DataApproval( dataApproval ); // (clone, so we don't modify a Hibernate object.)
+            dataApproval.setPeriod( longerPeriod );
+        }
     }
 
     /**

=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DefaultDataApprovalService.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DefaultDataApprovalService.java	2014-04-28 15:43:02 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DefaultDataApprovalService.java	2014-06-06 19:29:39 +0000
@@ -28,6 +28,7 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import java.util.Collection;
 import java.util.Set;
 
 import org.apache.commons.collections.CollectionUtils;
@@ -42,6 +43,7 @@
 import org.hisp.dhis.organisationunit.OrganisationUnitService;
 import org.hisp.dhis.period.Period;
 import org.hisp.dhis.period.PeriodService;
+import org.hisp.dhis.period.PeriodType;
 import org.hisp.dhis.security.SecurityService;
 import org.hisp.dhis.user.CurrentUserService;
 import org.hisp.dhis.user.User;
@@ -118,7 +120,22 @@
         if ( ( dataApproval.getCategoryOptionGroup() == null || securityService.canRead( dataApproval.getCategoryOptionGroup() ) )
             && mayApprove( dataApproval.getOrganisationUnit() ) )
         {
-            dataApprovalStore.addDataApproval( dataApproval );
+            PeriodType selectionPeriodType = dataApproval.getPeriod().getPeriodType();
+            PeriodType dataSetPeriodType = dataApproval.getDataSet().getPeriodType();
+
+            if ( selectionPeriodType.equals( dataSetPeriodType ) )
+            {
+                dataApprovalStore.addDataApproval( dataApproval );
+            }
+            else if ( selectionPeriodType.getFrequencyOrder() <= dataSetPeriodType.getFrequencyOrder() )
+            {
+                log.warn( "Attempted data approval for period " + dataApproval.getPeriod().getIsoDate()
+                + " is incompatible with data set period type " + dataSetPeriodType.getName() + "." );
+            }
+            else
+            {
+                approveCompositePeriod( dataApproval );
+            }
         }
         else
         {
@@ -131,18 +148,33 @@
         if ( ( dataApproval.getCategoryOptionGroup() == null || securityService.canRead( dataApproval.getCategoryOptionGroup() ) )
             && mayUnapprove( dataApproval.getOrganisationUnit(), dataApproval.isAccepted() ) )
         {
-            dataApprovalStore.deleteDataApproval( dataApproval );
+            PeriodType selectionPeriodType = dataApproval.getPeriod().getPeriodType();
+            PeriodType dataSetPeriodType = dataApproval.getDataSet().getPeriodType();
 
-            for ( OrganisationUnit ancestor : dataApproval.getOrganisationUnit().getAncestors() )
+            if ( selectionPeriodType.equals( dataSetPeriodType ) )
             {
-                DataApproval ancestorApproval = dataApprovalStore.getDataApproval(
-                    dataApproval.getDataSet(), dataApproval.getPeriod(), ancestor, dataApproval.getCategoryOptionGroup() );
+                dataApprovalStore.deleteDataApproval( dataApproval );
 
-                if ( ancestorApproval != null ) 
+                for ( OrganisationUnit ancestor : dataApproval.getOrganisationUnit().getAncestors() )
                 {
-                    dataApprovalStore.deleteDataApproval ( ancestorApproval );
+                    DataApproval ancestorApproval = dataApprovalStore.getDataApproval(
+                            dataApproval.getDataSet(), dataApproval.getPeriod(), ancestor, dataApproval.getCategoryOptionGroup() );
+
+                    if ( ancestorApproval != null )
+                    {
+                        dataApprovalStore.deleteDataApproval ( ancestorApproval );
+                    }
                 }
             }
+            else if ( selectionPeriodType.getFrequencyOrder() <= dataSetPeriodType.getFrequencyOrder() )
+            {
+                log.warn( "Attempted data unapproval for period " + dataApproval.getPeriod().getIsoDate()
+                        + " is incompatible with data set period type " + dataSetPeriodType.getName() + "." );
+            }
+            else
+            {
+                unapproveCompositePeriod( dataApproval );
+            }
         }
         else
         {
@@ -198,16 +230,18 @@
             && ( dataApprovalLevel.getCategoryOptionGroupSet() == null || securityService.canRead( dataApprovalLevel.getCategoryOptionGroupSet() ))
             && canReadOneCategoryOptionGroup( categoryOptionGroups ) )
         {
-            boolean accepted = false;
+            boolean unacceptPermissionNeededToUnapprove = false;
 
             switch ( status.getDataApprovalState() )
             {
+                case PARTIALLY_ACCEPTED_HERE:
+                case ACCEPTED_HERE:
+                    unacceptPermissionNeededToUnapprove = true;
+                case PARTIALLY_APPROVED_HERE:
                 case APPROVED_HERE:
-                case ACCEPTED_HERE:
-                    accepted = status.getDataApproval().isAccepted();
                 case UNAPPROVED_READY:
                     permissions.setMayApprove( mayApprove( organisationUnit ) );
-                    permissions.setMayUnapprove( mayUnapprove( organisationUnit, accepted ) );
+                    permissions.setMayUnapprove( mayUnapprove( organisationUnit, unacceptPermissionNeededToUnapprove ) );
                     permissions.setMayAccept( mayAcceptOrUnaccept( organisationUnit ) );
                     permissions.setMayUnaccept( permissions.isMayAccept() );
                     break;
@@ -226,38 +260,12 @@
 
     public void accept( DataApproval dataApproval )
     {
-        if ( ( dataApproval.getCategoryOptionGroup() == null || securityService.canRead( dataApproval.getCategoryOptionGroup() ) )
-            && mayAcceptOrUnaccept( dataApproval.getOrganisationUnit() ) )
-        {
-            if ( !dataApproval.isAccepted() )
-            {
-                dataApproval.setAccepted( true );
-
-                dataApprovalStore.updateDataApproval( dataApproval );
-            }
-        }
-        else
-        {
-            warnNotPermitted( dataApproval, "accept", mayAcceptOrUnaccept( dataApproval.getOrganisationUnit() ) );
-        }
+        acceptOrUnaccept( dataApproval, true );
     }
 
     public void unaccept( DataApproval dataApproval )
     {
-        if ( ( dataApproval.getCategoryOptionGroup() == null || securityService.canRead( dataApproval.getCategoryOptionGroup() ) )
-            && mayAcceptOrUnaccept( dataApproval.getOrganisationUnit() ) )
-        {
-            if ( dataApproval.isAccepted() )
-            {
-                dataApproval.setAccepted( false );
-
-                dataApprovalStore.updateDataApproval( dataApproval );
-            }
-        }
-        else
-        {
-            warnNotPermitted( dataApproval, "unaccept", mayAcceptOrUnaccept( dataApproval.getOrganisationUnit() ) );
-        }
+        acceptOrUnaccept( dataApproval, false );
     }
 
     // -------------------------------------------------------------------------
@@ -265,6 +273,128 @@
     // -------------------------------------------------------------------------
 
     /**
+     * Accept or unaccept a data approval.
+     *
+     * @param dataApproval the data approval object.
+     * @param accepted true to accept, false to unaccept.
+     */
+    public void acceptOrUnaccept ( DataApproval dataApproval, boolean accepted )
+    {
+        if ( ( dataApproval.getCategoryOptionGroup() == null || securityService.canRead( dataApproval.getCategoryOptionGroup() ) )
+                && mayAcceptOrUnaccept( dataApproval.getOrganisationUnit() ) )
+        {
+            PeriodType selectionPeriodType = dataApproval.getPeriod().getPeriodType();
+            PeriodType dataSetPeriodType = dataApproval.getDataSet().getPeriodType();
+
+            if ( selectionPeriodType.equals( dataSetPeriodType ) )
+            {
+                dataApproval.setAccepted( accepted );
+                dataApprovalStore.updateDataApproval( dataApproval );
+            } else if ( selectionPeriodType.getFrequencyOrder() <= dataSetPeriodType.getFrequencyOrder() )
+            {
+                log.warn( "Attempted data approval for period " + dataApproval.getPeriod().getIsoDate()
+                        + " is incompatible with data set period type " + dataSetPeriodType.getName() + "." );
+            } else
+            {
+                acceptOrUnacceptCompositePeriod( dataApproval, accepted );
+            }
+        } else
+        {
+            warnNotPermitted( dataApproval, accepted ? "accept" : "unaccept", mayAcceptOrUnaccept( dataApproval.getOrganisationUnit() ) );
+        }
+    }
+
+    /**
+     * Approves data for a longer period that contains multiple data approval
+     * periods. When individual periods are already approved, no action is
+     * necessary. (It's possible that they could be accepted as well.)
+     *
+     * @param da data approval object describing the longer period.
+     */
+    private void approveCompositePeriod( DataApproval da )
+    {
+        Collection<Period> periods = periodService.getPeriodsBetweenDates(
+                da.getDataSet().getPeriodType(),
+                da.getPeriod().getStartDate(),
+                da.getPeriod().getEndDate() );
+
+        for ( Period period : periods )
+        {
+            DataApprovalStatus status = getDataApprovalStatus( da.getDataSet(), period, da.getOrganisationUnit(),
+                    org.hisp.dhis.system.util.CollectionUtils.asSet( da.getCategoryOptionGroup() ), null );
+
+            if ( status.getDataApprovalState().isReady() && !status.getDataApprovalState().isApproved() )
+            {
+                DataApproval dataApproval = new DataApproval( da );
+                dataApproval.setPeriod( period );
+
+                dataApprovalStore.addDataApproval( dataApproval );
+            }
+        }
+    }
+
+    /**
+     * Unapproves data for a longer period that contains multiple data approval
+     * periods. When individual periods are already unapproved, no action is
+     * necessary.
+     * <p>
+     * Note that when we delete approval for a period, we also need to make
+     * sure that approval is removed for any ancestors at higher levels of
+     * approval. For this reason, we go back through the main deleteDataApproval
+     * method. (It won't call back here, becuase it's only for one period.)
+     *
+     * @param da data approval object describing the longer period.
+     */
+    void unapproveCompositePeriod( DataApproval da )
+    {
+        Collection<Period> periods = periodService.getPeriodsBetweenDates(
+                da.getDataSet().getPeriodType(),
+                da.getPeriod().getStartDate(),
+                da.getPeriod().getEndDate() );
+
+        for ( Period period : periods )
+        {
+            DataApprovalStatus status = getDataApprovalStatus( da.getDataSet(), period, da.getOrganisationUnit(),
+                    org.hisp.dhis.system.util.CollectionUtils.asSet( da.getCategoryOptionGroup() ), null );
+
+            if ( status.getDataApprovalState().isApproved() )
+            {
+                deleteDataApproval( status.getDataApproval() );
+            }
+        }
+    }
+
+    /**
+     * Accepts or unaccepts data for a longer period that contains multiple
+     * data approval periods. When individual periods are already at the
+     * desired accptance state, no action is necessary.
+     *
+     * @param da data approval object describing the longer period.
+     * @param accepted true to accept, false to unaccept.
+     */
+    private void acceptOrUnacceptCompositePeriod( DataApproval da, boolean accepted )
+    {
+        Collection<Period> periods = periodService.getPeriodsBetweenDates(
+                da.getDataSet().getPeriodType(),
+                da.getPeriod().getStartDate(),
+                da.getPeriod().getEndDate() );
+
+        DataApprovalLevel lowestApprovalLevel = null;
+
+        for ( Period period : periods )
+        {
+            DataApprovalStatus status = getDataApprovalStatus( da.getDataSet(), period, da.getOrganisationUnit(),
+                    org.hisp.dhis.system.util.CollectionUtils.asSet( da.getCategoryOptionGroup() ), null );
+
+            if ( status.getDataApprovalState().isApprovable() && status.getDataApprovalState().isAccepted() != accepted )
+            {
+                status.getDataApproval().setAccepted( accepted );
+                dataApprovalStore.updateDataApproval( status.getDataApproval() );
+            }
+        }
+    }
+
+    /**
      * Return true if there are no category option groups, or if there is
      * one and the user can read it.
      *
@@ -342,14 +472,15 @@
      * data set, and the user is not authorized to remove that approval as well.
      *
      * @param organisationUnit The data approval status to check for permission.
-     * @param accepted Whether selection is accepted.
+     * @param unacceptPermissionNeededToUnapprove Whether *unaccept* permission
+     *                                   is also needed to unapprove this data.
      * @return true if the user may unapprove, otherwise false
      */
-    private boolean mayUnapprove( OrganisationUnit organisationUnit, boolean accepted )
+    private boolean mayUnapprove( OrganisationUnit organisationUnit, boolean unacceptPermissionNeededToUnapprove )
     {
         if ( isAuthorizedToUnapprove( organisationUnit ) )
         {
-            if ( !accepted || mayAcceptOrUnaccept( organisationUnit ) )
+            if ( !unacceptPermissionNeededToUnapprove || mayAcceptOrUnaccept( organisationUnit ) )
             {
                 log.debug( "mayUnapprove = true for organisation unit " + organisationUnit.getName() );
 

=== modified file 'dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataapproval/DataApprovalServiceTest.java'
--- dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataapproval/DataApprovalServiceTest.java	2014-04-28 21:42:50 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataapproval/DataApprovalServiceTest.java	2014-06-06 19:29:39 +0000
@@ -723,8 +723,8 @@
         assertEquals( DataApprovalState.UNAPPROVED_WAITING, dataApprovalService.getDataApprovalStatus( dataSetA, periodA, organisationUnitA, NO_GROUPS, NO_OPTIONS ).getDataApprovalState() );
         assertEquals( DataApprovalState.UNAPPROVED_READY, dataApprovalService.getDataApprovalStatus( dataSetA, periodA, organisationUnitD, NO_GROUPS, NO_OPTIONS ).getDataApprovalState() );
         assertEquals( DataApprovalState.UNAPPROVED_WAITING, dataApprovalService.getDataApprovalStatus( dataSetA, periodB, organisationUnitA, NO_GROUPS, NO_OPTIONS ).getDataApprovalState() );
-        assertEquals( DataApprovalState.UNAPPROVED_ELSEWHERE, dataApprovalService.getDataApprovalStatus( dataSetA, periodC, organisationUnitA, NO_GROUPS, NO_OPTIONS ).getDataApprovalState() );
-        assertEquals( DataApprovalState.UNAPPROVED_ELSEWHERE, dataApprovalService.getDataApprovalStatus( dataSetA, periodC, organisationUnitD, NO_GROUPS, NO_OPTIONS ).getDataApprovalState() );
+        assertEquals( DataApprovalState.UNAPPROVED_WAITING, dataApprovalService.getDataApprovalStatus( dataSetA, periodC, organisationUnitA, NO_GROUPS, NO_OPTIONS ).getDataApprovalState() );
+        assertEquals( DataApprovalState.UNAPPROVED_READY, dataApprovalService.getDataApprovalStatus( dataSetA, periodC, organisationUnitD, NO_GROUPS, NO_OPTIONS ).getDataApprovalState() );
         assertEquals( DataApprovalState.UNAPPROVABLE, dataApprovalService.getDataApprovalStatus( dataSetA, periodD, organisationUnitD, NO_GROUPS, NO_OPTIONS ).getDataApprovalState() );
     }
 

=== modified file 'dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/util/CollectionUtils.java'
--- dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/util/CollectionUtils.java	2014-04-11 07:50:58 +0000
+++ dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/util/CollectionUtils.java	2014-06-06 19:29:39 +0000
@@ -28,9 +28,12 @@
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import java.util.AbstractMap;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.Map;
 import java.util.Set;
 
 import org.hisp.dhis.system.util.functional.Function1;
@@ -95,4 +98,34 @@
         
         return set;
     }
+
+    /**
+     * Constructs a Map Entry (key, value). Used to construct a Map with asMap.
+     *
+     * @param key map entry key
+     * @param value map entry value
+     * @return entry with the key and value
+     */
+    public static <K, V> AbstractMap.SimpleEntry<K, V> asEntry( K key, V value )
+    {
+        return new AbstractMap.SimpleEntry<K, V>( key, value );
+    }
+
+    /**
+     * Constructs a Map from Entries, each containing a (key, value) pair.
+     *
+     * @param entries any number of (key, value) pairs
+     * @return Map of the entries
+     */
+    public static <K, V> Map<K, V> asMap( AbstractMap.SimpleEntry<K, V>... entries )
+    {
+        Map<K, V> map = new HashMap<K, V>();
+
+        for ( AbstractMap.SimpleEntry<K, V> entry : entries )
+        {
+            map.put( entry.getKey(), entry.getValue() );
+        }
+
+        return map;
+    }
 }

=== modified file 'dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataApprovalController.java'
--- dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataApprovalController.java	2014-05-22 12:40:24 +0000
+++ dhis-2/dhis-web/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataApprovalController.java	2014-06-06 19:29:39 +0000
@@ -213,10 +213,13 @@
         }
 
         DataApprovalPermissions permissions = dataApprovalService.getDataApprovalPermissions( dataSet, period, organisationUnit, categoryOptionGroups, null );
-        
-        if ( !DataApprovalState.UNAPPROVED_READY.equals( permissions.getDataApprovalStatus().getDataApprovalState() ) )
+
+        DataApprovalState state = permissions.getDataApprovalStatus().getDataApprovalState();
+
+        if ( state != DataApprovalState.UNAPPROVED_READY &&
+             state != DataApprovalState.PARTIALLY_APPROVED_HERE )
         {
-            ContextUtils.conflictResponse( response, "Data is not ready for approval, current state is: " + permissions.getDataApprovalStatus().getDataApprovalState().name() );
+            ContextUtils.conflictResponse( response, "Data is not ready for approval here, current state is: " + state.name() );
             return;
         }
 
@@ -289,10 +292,12 @@
 
         DataApprovalState state = permissions.getDataApprovalStatus().getDataApprovalState();
 
-        if ( !DataApprovalState.APPROVED_HERE.equals( state )
-            && !DataApprovalState.ACCEPTED_HERE.equals(state ) )
+        if ( state != DataApprovalState.APPROVED_HERE &&
+             state != DataApprovalState.ACCEPTED_HERE &&
+             state != DataApprovalState.PARTIALLY_APPROVED_HERE &&
+             state != DataApprovalState.PARTIALLY_ACCEPTED_HERE )
         {
-            ContextUtils.conflictResponse( response, "Data is not approved here and cannot be unapproved" );
+            ContextUtils.conflictResponse( response, "Data is not approved here, current state is: " + state.name() );
             return;
         }
 
@@ -355,9 +360,12 @@
 
         DataApprovalPermissions permissions = dataApprovalService.getDataApprovalPermissions( dataSet, period, organisationUnit, categoryOptionGroups, null );
 
-        if ( !DataApprovalState.APPROVED_HERE.equals( permissions.getDataApprovalStatus().getDataApprovalState() ) )
+        DataApprovalState state = permissions.getDataApprovalStatus().getDataApprovalState();
+
+        if ( state != DataApprovalState.APPROVED_HERE &&
+             state != DataApprovalState.PARTIALLY_ACCEPTED_HERE )
         {
-            ContextUtils.conflictResponse( response, "Data is not approved here, current state is: " + permissions.getDataApprovalStatus().getDataApprovalState().name() );
+            ContextUtils.conflictResponse( response, "Data is not ready for accepting here, current state is: " + state.name() );
             return;
         }
 
@@ -421,9 +429,12 @@
 
         DataApprovalPermissions permissions = dataApprovalService.getDataApprovalPermissions( dataSet, period, organisationUnit, categoryOptionGroups, null );
 
-        if ( !DataApprovalState.ACCEPTED_HERE.equals( permissions.getDataApprovalStatus().getDataApprovalState() ) )
+        DataApprovalState state = permissions.getDataApprovalStatus().getDataApprovalState();
+
+        if ( state != DataApprovalState.ACCEPTED_HERE &&
+             state != DataApprovalState.PARTIALLY_ACCEPTED_HERE )
         {
-            ContextUtils.conflictResponse( response, "Data is not approved here, current state is: " + permissions.getDataApprovalStatus().getDataApprovalState().name() );
+            ContextUtils.conflictResponse( response, "Data is not accepted here, current state is: " + state.name() );
             return;
         }
 

=== modified file 'dhis-2/dhis-web/dhis-web-reporting/src/main/java/org/hisp/dhis/reporting/dataapproval/action/GetDataApprovalOptionsAction.java'
--- dhis-2/dhis-web/dhis-web-reporting/src/main/java/org/hisp/dhis/reporting/dataapproval/action/GetDataApprovalOptionsAction.java	2014-04-02 12:33:53 +0000
+++ dhis-2/dhis-web/dhis-web-reporting/src/main/java/org/hisp/dhis/reporting/dataapproval/action/GetDataApprovalOptionsAction.java	2014-06-06 19:29:39 +0000
@@ -37,12 +37,15 @@
 import org.hisp.dhis.dataelement.DataElementCategoryService;
 import org.hisp.dhis.dataset.DataSet;
 import org.hisp.dhis.dataset.DataSetService;
+import org.hisp.dhis.period.PeriodType;
 import org.hisp.dhis.system.util.Filter;
 import org.hisp.dhis.system.util.FilterUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 
 import com.opensymphony.xwork2.Action;
 
+import static org.hisp.dhis.period.PeriodType.getAvailablePeriodTypes;
+
 public class GetDataApprovalOptionsAction
     implements Action
 {
@@ -69,7 +72,14 @@
     {
         return dataSets;
     }
-    
+
+    private List<PeriodType> periodTypes;
+
+    public List<PeriodType> getPeriodTypes()
+    {
+        return periodTypes;
+    }
+
     // -------------------------------------------------------------------------
     // Action implementation
     // -------------------------------------------------------------------------
@@ -79,8 +89,9 @@
         throws Exception
     {
         categoryOptionGroups = new ArrayList<CategoryOptionGroup>( categoryService.getAllCategoryOptionGroups() );
-        dataSets = new ArrayList<DataSet>( dataSetService.getAllDataSets() );        
-        
+        dataSets = new ArrayList<DataSet>( dataSetService.getAllDataSets() );
+        periodTypes = getAvailablePeriodTypes();
+
         FilterUtils.filter( dataSets, new DataSetApproveDataFilter() );
         
         Collections.sort( categoryOptionGroups, IdentifiableObjectNameComparator.INSTANCE );

=== modified file 'dhis-2/dhis-web/dhis-web-reporting/src/main/resources/org/hisp/dhis/reporting/i18n_module.properties'
--- dhis-2/dhis-web/dhis-web-reporting/src/main/resources/org/hisp/dhis/reporting/i18n_module.properties	2014-05-26 10:46:17 +0000
+++ dhis-2/dhis-web/dhis-web-reporting/src/main/resources/org/hisp/dhis/reporting/i18n_module.properties	2014-06-06 19:29:39 +0000
@@ -261,6 +261,10 @@
 waiting_for_approval_elsewhere=Waiting for approval elsewhere
 approved_elsewhere=Approved elsewhere
 accepted_elsewhere=Accepted elsewhere
+approved_for_part_of_this_period=Approved for part of this period
+accepted_for_part_of_this_period=Accepted for part of this period
+approved_elsewhere_for_part_of_this_period=Approved elsewhere for part of this period
+accepted_elsewhere_for_part_of_this_period=Accepted elsewhere for part of this period
 confirm_approval=Are you sure you want to approve this data set?
 confirm_unapproval=Are you sure you want to unapprove this data set?
 confirm_accept=Are you sure you want to accept this data set approval?

=== modified file 'dhis-2/dhis-web/dhis-web-reporting/src/main/webapp/dhis-web-reporting/dataApprovalForm.vm'
--- dhis-2/dhis-web/dhis-web-reporting/src/main/webapp/dhis-web-reporting/dataApprovalForm.vm	2014-05-26 10:46:17 +0000
+++ dhis-2/dhis-web/dhis-web-reporting/src/main/webapp/dhis-web-reporting/dataApprovalForm.vm	2014-06-06 19:29:39 +0000
@@ -1,8 +1,18 @@
 <script type="text/javascript">
 
+dhis2.appr.metaData = {
+  "periodTypes": [
+    #set( $size1 = $periodTypes.size() )
+    #foreach( $type in $periodTypes )
+      "${type.name}"#if( $velocityCount < $size1 ),#end
+    #end
+  ]
+};
+
 selection.setListenerFunction( dhis2.appr.orgUnitSelected  );
 
 var i18n_select_data_set = '$encoder.jsEscape( $i18n.getString( "select_data_set" ), "'")';
+var i18n_select_period_type = '$encoder.jsEscape( $i18n.getString( "select_period_type" ), "'")';
 var i18n_select_period = '$encoder.jsEscape( $i18n.getString( "select_period" ), "'")';
 var i18n_select_organisation_unit = '$encoder.jsEscape( $i18n.getString( "select_organisation_unit" ), "'")';
 var i18n_generating_report = '$encoder.jsEscape( $i18n.getString( "generating_report" ), "'")';    
@@ -15,6 +25,10 @@
 var i18n_approved_elsewhere = '$encoder.jsEscape( $i18n.getString( "approved_elsewhere" ) , "'")';
 var i18n_accepted_elsewhere = '$encoder.jsEscape( $i18n.getString( "accepted_elsewhere" ) , "'")';
 var i18n_approved_and_accepted = '$encoder.jsEscape( $i18n.getString( "approved_and_accepted" ) , "'")';
+var i18n_approved_for_part_of_this_period = '$encoder.jsEscape( $i18n.getString( "approved_for_part_of_this_period" ) , "'")';
+var i18n_accepted_for_part_of_this_period = '$encoder.jsEscape( $i18n.getString( "accepted_for_part_of_this_period" ) , "'")';
+var i18n_approved_elsewhere_for_part_of_this_period = '$encoder.jsEscape( $i18n.getString( "approved_elsewhere_for_part_of_this_period" ) , "'")';
+var i18n_accepted_elsewhere_for_part_of_this_period = '$encoder.jsEscape( $i18n.getString( "accepted_elsewhere_for_part_of_this_period" ) , "'")';
 var i18n_confirm_approval = '$encoder.jsEscape( $i18n.getString( "confirm_approval" ) , "'")';
 var i18n_confirm_unapproval = '$encoder.jsEscape( $i18n.getString( "confirm_unapproval" ) , "'")';
 var i18n_confirm_accept = '$encoder.jsEscape( $i18n.getString( "confirm_accept" ) , "'")';
@@ -55,7 +69,7 @@
 <input type="button" id="approveButton" value="$i18n.getString( 'approve' )" onclick="dhis2.appr.approveData()" class="approveButton" style="width:120px">
 <input type="button" id="unapproveButton" value="$i18n.getString( 'unapprove' )" onclick="dhis2.appr.unapproveData()" class="approveButton" style="width:120px">
 <input type="button" id="acceptButton" value="$i18n.getString( 'accept' )" onclick="dhis2.appr.acceptData()" class="approveButton" style="width:120px">
-<input type="button" id="unacceptButton" value="$i18n.getString( 'unaccept' )" onclick="dhis2.appr.unacceptData()" class="approveButton" style="width:120px">
+<input type="button" id="unacceptButton" value="$i18n.getString( 'unaccept' )" onclick="dhis2.appr.unacceptData()" class="approveButton" style="width:120px">
 </div>
 
 <div id="criteria" class="inputCriteria" style="width:360px;">
@@ -78,6 +92,7 @@
 
 <div class="inputSection">
 <label>$i18n.getString( "report_period" )</label><br>
+<select id="periodType" name="periodType" style="width:174px" disabled="disabled" onchange="dhis2.appr.displayPeriods()"></select>
 <input type="button" style="width:75px" value="$i18n.getString( 'prev_year' )" onclick="dhis2.appr.displayPreviousPeriods()" />
 <input type="button" style="width:75px" value="$i18n.getString( 'next_year' )" onclick="dhis2.appr.displayNextPeriods()" /><br>
 <select id="periodId" name="periodId" style="width:330px" disabled="disabled">

=== modified file 'dhis-2/dhis-web/dhis-web-reporting/src/main/webapp/dhis-web-reporting/javascript/dataApproval.js'
--- dhis-2/dhis-web/dhis-web-reporting/src/main/webapp/dhis-web-reporting/javascript/dataApproval.js	2014-05-26 10:46:17 +0000
+++ dhis-2/dhis-web/dhis-web-reporting/src/main/webapp/dhis-web-reporting/javascript/dataApproval.js	2014-06-06 19:29:39 +0000
@@ -12,9 +12,33 @@
 // Report
 //------------------------------------------------------------------------------
 
+/**
+ * Callback for changes in data set. Displays a list of period types starting
+ * with the data set's period as the shortest type, and including all longer
+ * types so that approvals can be made for multiple periods. If there is a
+ * current period type selection, and it is still on the new list of period
+ * types, keep it. Otherwise choose the period type for the data set.
+ */
 dhis2.appr.dataSetSelected = function()
 {
-	dhis2.appr.displayPeriods();
+    var dataSetPeriodType = $( "#dataSetId :selected" ).data( "pt" );
+    var periodTypeToSelect = $( "#periodType" ).val() || dataSetPeriodType;
+    var foundDataSetPeriodType = false;
+    var html = "<option value=''>[ " + i18n_select_period_type + " ]</option>";
+
+    $.each( dhis2.appr.metaData.periodTypes, function() {
+        if ( foundDataSetPeriodType || this == dataSetPeriodType ) {
+            var selected = ( this == periodTypeToSelect ) ? " selected" : "";
+            html += "<option value='" + this + "'" + selected + ">" + this + "</option>";
+            foundDataSetPeriodType = true;
+        } else if ( this == periodTypeToSelect ) {
+            periodTypeToSelect = dataSetPeriodType;
+        }
+    } );
+
+    $( "#periodType" ).html( html );
+    $( "#periodType" ).removeAttr( "disabled" );
+    dhis2.appr.displayPeriods();
 }
 
 dhis2.appr.orgUnitSelected = function( orgUnits, orgUnitNames, children )
@@ -24,8 +48,8 @@
 
 dhis2.appr.displayPeriods = function()
 {
-	var pt = $( '#dataSetId :selected' ).data( "pt" );
-	dhis2.dsr.displayPeriodsInternal( pt, dhis2.appr.currentPeriodOffset );
+    var periodType = $( "#periodType" ).val();
+    dhis2.dsr.displayPeriodsInternal( periodType, dhis2.appr.currentPeriodOffset );
 }
 
 dhis2.appr.displayNextPeriods = function()
@@ -164,7 +188,22 @@
 		        }
 		        
 		        break;
-		
+
+            case "PARTIALLY_APPROVED_HERE":
+                $( "#approvalNotification" ).html( i18n_approved_for_part_of_this_period );
+
+                if ( json.mayApprove ) {
+                    $( "#approvalDiv" ).show();
+                    $( "#approveButton" ).show();
+                }
+
+                if ( json.mayUnapprove )  {
+                    $( "#approvalDiv" ).show();
+                    $( "#unapproveButton" ).show();
+                }
+
+                break;
+
 		    case "APPROVED_HERE":
 		        $( "#approvalNotification" ).html( i18n_approved );
 		        
@@ -179,11 +218,35 @@
 		        }
 		        
 		        break;
-		
+
+            case "PARTIALLY_APPROVED_ELSEWHERE":
+                $( "#approvalNotification" ).html( i18n_approved_elsewhere_for_part_of_this_period );
+                break;
+
 		    case "APPROVED_ELSEWHERE":
 		        $( "#approvalNotification" ).html( i18n_approved_elsewhere );
 		        break;
-		        
+
+            case "PARTIALLY_ACCEPTED_HERE":
+                $( "#approvalNotification" ).html( i18n_accepted_for_part_of_this_period );
+
+                if ( json.mayUnapprove )  {
+                    $( "#approvalDiv" ).show();
+                    $( "#unapproveButton" ).show();
+                }
+
+                if ( json.mayAccept )  {
+                    $( "#approvalDiv" ).show();
+                    $( "#acceptButton" ).show();
+                }
+
+                if ( json.mayUnaccept )  {
+                    $( "#approvalDiv" ).show();
+                    $( "#unacceptButton" ).show();
+                }
+
+                break;
+
 		    case "ACCEPTED_HERE":
 		        $( "#approvalNotification" ).html( i18n_approved_and_accepted );
 		        
@@ -192,13 +255,17 @@
 		            $( "#unapproveButton" ).show();
 		        }
 		        
-		        if ( json.mayUnccept )  {
+		        if ( json.mayUnaccept )  {
 		            $( "#approvalDiv" ).show();
 		            $( "#unacceptButton" ).show();
 		        }
 		        
 		        break;
 
+            case "PARTIALLY_ACCEPTED_ELSEWHERE":
+                $( "#approvalNotification" ).html( i18n_accepted_elsewhere_for_part_of_this_period );
+                break;
+
 	        case "ACCEPTED_ELSEWHERE":
 		        $( "#approvalNotification" ).html( i18n_accepted_elsewhere );
 		        break;
@@ -217,18 +284,8 @@
 		url: dhis2.appr.getApprovalUrl(),
 		type: "post",
 		success: function() {
-            $( "#approvalNotification" ).show().html( i18n_approved );
-            $( "#approvalDiv" ).hide();
-			$( "#approveButton" ).hide();
-            if ( dhis2.appr.permissions.mayUnapprove ) {
-                $( "#approvalDiv" ).show();
-                $( "#unapproveButton" ).show();
-            }
-            if ( dhis2.appr.permissions.mayAccept ) {
-                $( "#approvalDiv" ).show();
-                $( "#acceptButton" ).show();
-            }
-		},
+            dhis2.appr.setApprovalState();
+        },
 		error: function( xhr, status, error ) {
 			alert( xhr.responseText );
 		}
@@ -245,17 +302,8 @@
 		url: dhis2.appr.getApprovalUrl(),
 		type: "delete",
 		success: function() {
-            $( "#approvalNotification" ).show().html( i18n_ready_for_approval );
-            $( "#approvalDiv" ).hide();
-            $( "#unapproveButton" ).hide();
-            $( "#acceptButton" ).hide();
-            $( "#unacceptButton" ).hide();
-            
-            if ( dhis2.appr.permissions.mayApprove ) {
-                $( "#approvalDiv" ).show();
-                $( "#approveButton" ).show();
-            }
-		},
+            dhis2.appr.setApprovalState();
+        },
 		error: function( xhr, status, error ) {
 			alert( xhr.responseText );
 		}
@@ -272,19 +320,7 @@
 		url: dhis2.appr.getAcceptanceUrl(),
         type: "post",
         success: function() {
-            $( "#approvalNotification" ).show().html( i18n_approved_and_accepted );
-            $( "#approvalDiv" ).hide();
-            $( "#acceptButton" ).hide();
-          
-            if ( dhis2.appr.permissions.mayUnapprove ) {
-                $( "#approvalDiv" ).show();
-                $( "#unapproveButton" ).show();
-            }
-          
-            if ( dhis2.appr.permissions.mayUnaccept ) {
-                $( "#approvalDiv" ).show();
-                $( "#unacceptButton" ).show();
-            }
+            dhis2.appr.setApprovalState();
         },
         error: function( xhr, status, error ) {
             alert( xhr.responseText );
@@ -302,17 +338,7 @@
 		url: dhis2.appr.getAcceptanceUrl(),
         type: "delete",
         success: function() {
-            $( "#approvalNotification" ).show().html( i18n_approved );
-            $( "#approvalDiv" ).hide();
-            $( "#unacceptButton" ).hide();
-            if ( dhis2.appr.permissions.mayUnapprove ) {
-                $( "#approvalDiv" ).show();
-                $( "#unapproveButton" ).show();
-            }
-            if ( dhis2.appr.permissions.mayAccept ) {
-                $( "#approvalDiv" ).show();
-                $( "#acceptButton" ).show();
-            }
+            dhis2.appr.setApprovalState();
         },
         error: function( xhr, status, error ) {
             alert( xhr.responseText );