← Back to team overview

dhis2-devs team mailing list archive

[Branch ~dhis2-devs-core/dhis2/trunk] Rev 13427: Merged branch dataset-approval from Jim Grace. Implements service layer for data approval by data...

 

Merge authors:
  Jim Grace (jimgrace)
  Lars Helge Øverland (larshelge)
------------------------------------------------------------
revno: 13427 [merge]
committer: Lars Helge Øverland <larshelge@xxxxxxxxx>
branch nick: dhis2
timestamp: Wed 2013-12-25 16:01:48 +0100
message:
  Merged branch dataset-approval from Jim Grace. Implements service layer for data approval by data set.
added:
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApproval.java
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalService.java
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalState.java
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalStore.java
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/
  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/hibernate/
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalStore.java
  dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataapproval/
  dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataapproval/hibernate/
  dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataapproval/hibernate/DataApproval.hbm.xml
  dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataapproval/
  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/DataApprovalStoreTest.java
modified:
  .bzrignore
  dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSet.java
  dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/startup/TableAlteror.java
  dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/beans.xml
  dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataset/hibernate/DataSet.hbm.xml
  dhis-2/dhis-support/dhis-support-jdbc/src/main/java/org/hisp/dhis/jdbc/batchhandler/DataSetBatchHandler.java
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/java/org/hisp/dhis/dataset/action/AddDataSetAction.java
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/java/org/hisp/dhis/dataset/action/UpdateDataSetAction.java
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/resources/org/hisp/dhis/dataset/i18n_module.properties
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/webapp/dhis-web-maintenance-dataset/addDataSet.vm
  dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/webapp/dhis-web-maintenance-dataset/editDataSet.vm


--
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 '.bzrignore'
--- .bzrignore	2013-05-12 17:11:10 +0000
+++ .bzrignore	2013-12-06 05:00:46 +0000
@@ -25,3 +25,4 @@
 ./.gitattributes
 ./.gitignore
 ./.travis.yml
+.DS_Store

=== added directory 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval'
=== added 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	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApproval.java	2013-12-25 15:01:48 +0000
@@ -0,0 +1,178 @@
+package org.hisp.dhis.dataapproval;
+
+/*
+ * Copyright (c) 2004-2013, 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 java.io.Serializable;
+import java.util.Date;
+
+import org.hisp.dhis.dataelement.DataElementCategoryOptionCombo;
+import org.hisp.dhis.dataset.DataSet;
+import org.hisp.dhis.organisationunit.OrganisationUnit;
+import org.hisp.dhis.period.Period;
+import org.hisp.dhis.user.User;
+
+/**
+ * Records the approval of DataSet values for a given OrganisationUnit and
+ * Period.
+ * 
+ * @author Jim Grace
+ */
+public class DataApproval
+    implements Serializable
+{
+    private static final long serialVersionUID = -4034531921928532366L;
+
+    /**
+     * The DataSet for the values being approved.
+     */
+    private int id;
+
+    /**
+     * The DataSet for the values being approved.
+     */
+    private DataSet dataSet;
+
+    /**
+     * The Period of the DataSet values being approved.
+     */
+    private Period period;
+
+    /**
+     * The OrganisationUnit of the DataSet values being approved.
+     */
+    private OrganisationUnit organisationUnit;
+    
+    /**
+     * The attribute DataElementCategoryOptionCombo being approved.
+     */
+    private DataElementCategoryOptionCombo attributeOptionCombo;
+
+    /**
+     * The Date (including time) when the DataSet values were approved.
+     */
+    private Date created;
+
+    /**
+     * The User who approved the DataSet values.
+     */
+    private User creator;
+
+    // -------------------------------------------------------------------------
+    // Constructors
+    // -------------------------------------------------------------------------
+
+    public DataApproval()
+    {
+    }
+
+    public DataApproval( DataSet dataSet, Period period, OrganisationUnit organisationUnit, 
+        DataElementCategoryOptionCombo attributeOptionCombo, Date created, User creator )
+    {
+        this.dataSet = dataSet;
+        this.period = period;
+        this.organisationUnit = organisationUnit;
+        this.attributeOptionCombo = attributeOptionCombo;
+        this.created = created;
+        this.creator = creator;
+    }
+
+    // -------------------------------------------------------------------------
+    // Getters and setters
+    // -------------------------------------------------------------------------
+
+    public int getId()
+    {
+        return id;
+    }
+
+    public void setId( int id )
+    {
+        this.id = id;
+    }
+
+    public DataSet getDataSet()
+    {
+        return dataSet;
+    }
+
+    public void setDataSet( DataSet dataSet )
+    {
+        this.dataSet = dataSet;
+    }
+
+    public Period getPeriod()
+    {
+        return period;
+    }
+
+    public void setPeriod( Period period )
+    {
+        this.period = period;
+    }
+
+    public OrganisationUnit getOrganisationUnit()
+    {
+        return organisationUnit;
+    }
+
+    public void setOrganisationUnit( OrganisationUnit organisationUnit )
+    {
+        this.organisationUnit = organisationUnit;
+    }
+
+    public DataElementCategoryOptionCombo getAttributeOptionCombo()
+    {
+        return attributeOptionCombo;
+    }
+
+    public void setAttributeOptionCombo( DataElementCategoryOptionCombo attributeOptionCombo )
+    {
+        this.attributeOptionCombo = attributeOptionCombo;
+    }
+
+    public Date getCreated()
+    {
+        return created;
+    }
+
+    public void setCreated( Date created )
+    {
+        this.created = created;
+    }
+
+    public User getCreator()
+    {
+        return creator;
+    }
+
+    public void setCreator( User creator )
+    {
+        this.creator = creator;
+    }
+}

=== added file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalService.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalService.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalService.java	2013-12-25 15:01:48 +0000
@@ -0,0 +1,126 @@
+package org.hisp.dhis.dataapproval;
+
+/*
+ * Copyright (c) 2004-2013, 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.dataelement.DataElementCategoryOptionCombo;
+import org.hisp.dhis.dataset.DataSet;
+import org.hisp.dhis.organisationunit.OrganisationUnit;
+import org.hisp.dhis.period.Period;
+import org.hisp.dhis.user.User;
+
+/**
+ * @author Jim Grace
+ * @version $Id$
+ */
+public interface DataApprovalService
+{
+    String ID = DataApprovalService.class.getName();
+
+    /**
+     * Adds a DataApproval in order to approve data.
+     *
+     * @param dataApproval the DataApproval to add.
+     */
+    void addDataApproval( DataApproval dataApproval );
+
+    /**
+     * Deletes a DataApproval in order to un-approve data.
+     * Any higher-level DataApprovals above this organisation unit
+     * are also deleted for the same period and data set.
+     *
+     * @param dataApproval the DataApproval to delete.
+     */
+    void deleteDataApproval( DataApproval dataApproval );
+
+    /**
+     * Returns the DataApproval object (if any) for a given
+     * dataset, period and organisation unit.
+     *
+     * @param dataSet DataSet for approval
+     * @param period Period for approval
+     * @param organisationUnit OrganisationUnit for approval
+     * @param attributeOptionCombo DataElementCategoryOptionCombo for approval.
+     * @return matching DataApproval object, if any
+     */
+    DataApproval getDataApproval( DataSet dataSet, Period period, 
+        OrganisationUnit organisationUnit, DataElementCategoryOptionCombo attributeOptionCombo );
+    
+    /**
+     * Returns the DataApprovalState for a given data set, period and
+     * OrganisationUnit.
+     *
+     * @param dataSet DataSet to check for approval.
+     * @param period Period to check for approval.
+     * @param organisationUnit OrganisationUnit to check for approval.
+     * @return the data approval state.
+     */
+    DataApprovalState getDataApprovalState( DataSet dataSet, Period period, 
+        OrganisationUnit organisationUnit, DataElementCategoryOptionCombo attributeOptionCombo );
+
+    /**
+     * Checks to see whether a user may approve data for a given
+     * organisation unit.
+     *
+     * @param organisationUnit OrganisationUnit to check for approval.
+     * @param user The current user.
+     * @param mayApproveAtSameLevel Tells whether the user has the authority
+     *        to approve data for the user's assigned organisation unit(s).
+     * @param mayApproveAtLowerLevels Tells whether the user has the authority
+     *        to approve data below the user's assigned organisation unit(s).
+     * @return true if the user may approve, otherwise false
+     */
+    boolean mayApprove( OrganisationUnit organisationUnit, User user,
+        boolean mayApproveAtSameLevel, boolean mayApproveAtLowerLevels );
+
+    /**
+     * Checks to see whether a user may unapprove a given data approval.
+     * <p>
+     * A user may unapprove data for organisation unit A if they have the
+     * authority to approve data for organisation unit B, and B is an
+     * ancestor of A.
+     * <p>
+     * A user may also unapprove data for organisation unit A if they have
+     * the authority to approve data for organisation unit A, and A has no
+     * ancestors.
+     * <p>
+     * But a user may not unapprove data for an organisation unit if the data
+     * has been approved already at a higher level for the same period and
+     * data set, and the user is not authorized to remove that approval as well.
+     *
+     * @param dataApproval The data approval to check for access.
+     * @param user The current user.
+     * @param mayApproveAtSameLevel Tells whether the user has the authority
+     *        to approve data for the user's assigned organisation unit(s).
+     * @param mayApproveAtLowerLevels Tells whether the user has the authority
+     *        to approve data below the user's assigned organisation unit(s).
+     * @return true if the user may unapprove, otherwise false
+     */
+    boolean mayUnapprove( DataApproval dataApproval, User user,
+        boolean mayApproveAtSameLevel, boolean mayApproveAtLowerLevels );
+}

=== added 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	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalState.java	2013-12-21 01:36:12 +0000
@@ -0,0 +1,70 @@
+package org.hisp.dhis.dataapproval;
+
+/*
+ * Copyright (c) 2004-2013, 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.
+ */
+
+/**
+ * Current state of data approval for a given combination of data set, period
+ * and organisation unit.
+ *
+ * @author Jim Grace
+ * @version $Id$
+ */
+
+public enum DataApprovalState
+{
+    /**
+     * Data in this data set is approved for this period and organisation unit.
+     */
+    APPROVED,
+
+    /**
+     * Data in this data set is ready to be approved for this period and
+     * organisation unit.
+     */
+    READY_FOR_APPROVAL,
+
+    /**
+     * Data in this data set is not yet ready to be approved for this period
+     * and organisation unit, because it is waiting for approval at a
+     * lower-level organisation unit under this one.
+     */
+    WAITING_FOR_LOWER_LEVEL_APPROVAL,
+
+    /**
+     * Data in this data set does not need approval for this period and
+     * organisation unit, for one of the following reasons:
+     * <ul>
+     *     <li>Data approval is not enabled globally.</li>
+     *     <li>Data approval is not enabled for this data set.</li>
+     *     <li>No data is collected for this data set for this organisation
+     *         unit or any lower-level organisation units under it.</li>
+     * </ul>
+     */
+    APPROVAL_NOT_NEEDED
+}

=== added file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalStore.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalStore.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalStore.java	2013-12-25 15:01:48 +0000
@@ -0,0 +1,76 @@
+package org.hisp.dhis.dataapproval;
+
+/*
+ * Copyright (c) 2004-2013, 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.dataelement.DataElementCategoryOptionCombo;
+import org.hisp.dhis.dataset.DataSet;
+import org.hisp.dhis.organisationunit.OrganisationUnit;
+import org.hisp.dhis.period.Period;
+
+/**
+ * Defines the functionality for persisting DataApproval objects.
+ *
+ * @author Jim Grace
+ */
+public interface DataApprovalStore
+//        extends GenericStore<DataApproval>
+{
+    String ID = DataApprovalStore.class.getName();
+
+    // -------------------------------------------------------------------------
+    // Basic DataApproval
+    // -------------------------------------------------------------------------
+
+    /**
+     * Adds a DataApproval in order to approve data.
+     *
+     * @param dataApproval the DataApproval to add.
+     */
+    void addDataApproval( DataApproval dataApproval );
+
+    /**
+     * Deletes a DataApproval in order to un-approve data.
+     *
+     * @param dataApproval the DataApproval to delete.
+     */
+    void deleteDataApproval( DataApproval dataApproval );
+
+    /**
+     * Returns the DataApproval object (if any) for a given
+     * dataset, period and organisation unit.
+     *
+     * @param dataSet DataSet for approval
+     * @param period Period for approval
+     * @param organisationUnit OrganisationUnit for approval
+     * @param attributeOptionCombo DataElementCategoryOptionCombo for approval.
+     * @return matching DataApproval object, if any
+     */
+    DataApproval getDataApproval( DataSet dataSet, Period period, 
+        OrganisationUnit organisationUnit, DataElementCategoryOptionCombo attributeOptionCombo );
+}

=== modified file 'dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSet.java'
--- dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSet.java	2013-12-20 14:53:40 +0000
+++ dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSet.java	2013-12-22 21:08:30 +0000
@@ -174,6 +174,11 @@
      */
     private boolean notifyCompletingUser;
 
+    /**
+     * Indicating whether to approve data for this data set.
+     */
+    private boolean approveData;
+
     // -------------------------------------------------------------------------
     // Form properties
     // -------------------------------------------------------------------------
@@ -691,6 +696,19 @@
     @JsonProperty
     @JsonView({ DetailedView.class, ExportView.class, WithoutOrganisationUnitsView.class })
     @JacksonXmlProperty( namespace = DxfNamespaces.DXF_2_0 )
+    public boolean isApproveData()
+    {
+        return approveData;
+    }
+
+    public void setApproveData( boolean approveData )
+    {
+        this.approveData = approveData;
+    }
+
+    @JsonProperty
+    @JsonView( { DetailedView.class, ExportView.class } )
+    @JacksonXmlProperty( namespace = DxfNamespaces.DXF_2_0 )
     public boolean isAllowFuturePeriods()
     {
         return allowFuturePeriods;

=== added directory 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval'
=== added 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	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/DefaultDataApprovalService.java	2013-12-25 15:01:48 +0000
@@ -0,0 +1,218 @@
+package org.hisp.dhis.dataapproval;
+
+/*
+ * Copyright (c) 2004-2013, 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.apache.commons.collections.CollectionUtils;
+import org.hisp.dhis.dataelement.DataElementCategoryOptionCombo;
+import org.hisp.dhis.dataset.DataSet;
+import org.hisp.dhis.organisationunit.OrganisationUnit;
+import org.hisp.dhis.period.Period;
+import org.hisp.dhis.user.User;
+
+/**
+ * @author Jim Grace
+ */
+public class DefaultDataApprovalService
+    implements DataApprovalService
+{
+    // -------------------------------------------------------------------------
+    // Dependencies
+    // -------------------------------------------------------------------------
+
+    private DataApprovalStore dataApprovalStore;
+
+    public void setDataApprovalStore( DataApprovalStore dataApprovalStore )
+    {
+        this.dataApprovalStore = dataApprovalStore;
+    }
+
+    // -------------------------------------------------------------------------
+    // DataApproval
+    // -------------------------------------------------------------------------
+
+    public void addDataApproval( DataApproval dataApproval )
+    {
+        dataApprovalStore.addDataApproval( dataApproval );
+    }
+
+    public void deleteDataApproval( DataApproval dataApproval )
+    {
+        dataApprovalStore.deleteDataApproval( dataApproval );
+
+        for ( OrganisationUnit ancestor : dataApproval.getOrganisationUnit().getAncestors() )
+        {
+            DataApproval ancestorApproval = dataApprovalStore.getDataApproval(
+                    dataApproval.getDataSet(), dataApproval.getPeriod(), ancestor, dataApproval.getAttributeOptionCombo() );
+
+            if ( ancestorApproval != null ) {
+                dataApprovalStore.deleteDataApproval ( ancestorApproval );
+            }
+        }
+    }
+
+    public DataApproval getDataApproval( DataSet dataSet, Period period, OrganisationUnit organisationUnit, DataElementCategoryOptionCombo attributeOptionCombo )
+    {
+        return dataApprovalStore.getDataApproval( dataSet, period, organisationUnit, attributeOptionCombo );
+    }
+
+    public DataApprovalState getDataApprovalState( DataSet dataSet, Period period, OrganisationUnit organisationUnit, DataElementCategoryOptionCombo attributeOptionCombo )
+    {
+        if ( !dataSet.isApproveData() )
+        {
+            return DataApprovalState.APPROVAL_NOT_NEEDED;
+        }
+
+        if ( null != dataApprovalStore.getDataApproval( dataSet, period, organisationUnit, attributeOptionCombo ) )
+        {
+            return DataApprovalState.APPROVED;
+        }
+
+        boolean approvedAtLowerLevels = false; // Until proven otherwise
+
+        for ( OrganisationUnit child : organisationUnit.getChildren() )
+        {
+            switch ( getDataApprovalState( dataSet, period, child, attributeOptionCombo ) )
+            {
+                //
+                // If ready or waiting at a lower level, return
+                // WAITING_FOR_LOWER_LEVEL_APPROVAL at this level.
+                //
+                case READY_FOR_APPROVAL:
+                case WAITING_FOR_LOWER_LEVEL_APPROVAL:
+                    return DataApprovalState.WAITING_FOR_LOWER_LEVEL_APPROVAL;
+
+                case APPROVED:
+                    approvedAtLowerLevels = true;
+                    break;
+
+                case APPROVAL_NOT_NEEDED:
+                    break; // Do nothing.
+            }
+
+        }
+
+        //
+        // If approved at lower levels (and not ready or waiting at any),
+        // and/or if data is configured for entry at this level (whether or
+        // not it has been entered), return READY_FOR_APPROVAL.
+        //
+        if ( approvedAtLowerLevels ||
+             organisationUnit.getAllDataSets().contains ( dataSet ) )
+        {
+            return DataApprovalState.READY_FOR_APPROVAL;
+        }
+
+        //
+        // Finally, if we haven't seen any approval action at lower levels,
+        // and this level is not configured for data entry from this data set,
+        // then return APPROVAL_NOT_NEEDED.
+        //
+        return DataApprovalState.APPROVAL_NOT_NEEDED;
+    }
+
+    public boolean mayApprove( OrganisationUnit source, User user,
+        boolean mayApproveAtSameLevel, boolean mayApproveAtLowerLevels )
+    {
+        if ( mayApproveAtSameLevel && user.getOrganisationUnits().contains( source ) )
+        {
+            return true;
+        }
+
+        if ( mayApproveAtLowerLevels && CollectionUtils.containsAny( user.getOrganisationUnits(), source.getAncestors() ) )
+        {
+            return true;
+        }
+
+        return false;
+    }
+
+    public boolean mayUnapprove( DataApproval dataApproval, User user,
+        boolean mayApproveAtSameLevel, boolean mayApproveAtLowerLevels )
+    {
+        if ( isAuthorizedToUnapprove( dataApproval.getOrganisationUnit(), user, mayApproveAtSameLevel, mayApproveAtLowerLevels ) )
+        {
+            // Check approvals at higher levels that may block this unapproval:
+
+            for ( OrganisationUnit ancestor : dataApproval.getOrganisationUnit().getAncestors() )
+            {
+                DataApproval ancestorDataApproval = dataApprovalStore.getDataApproval(
+                        dataApproval.getDataSet(), dataApproval.getPeriod(), ancestor, dataApproval.getAttributeOptionCombo() );
+                
+                if ( ancestorDataApproval != null &&
+                    !isAuthorizedToUnapprove( ancestor, user, mayApproveAtSameLevel, mayApproveAtLowerLevels ) )
+                {
+                    return false; // Could unapprove at that level, but higher-level approval is blocking.
+                }
+            }
+
+            return true; // May unapprove at that level, and no higher-level approval is blocking.
+        }
+
+        return false; // May not unapprove at that level.
+    }
+
+    // -------------------------------------------------------------------------
+    // Supportive methods
+    // -------------------------------------------------------------------------
+
+    /**
+     * Tests whether the user is authorized to unapprove for this organisation
+     * unit.
+     * <p>
+     * Whether the user actually may unapprove an existing approval depends
+     * also on whether there are higher-level approvals that the user is
+     * authorized to unapprove.
+     *
+     * @param source OrganisationUnit to check for approval.
+     * @param user The current user.
+     * @param mayApproveAtSameLevel Tells whether the user has the authority
+     *        to approve data for the user's assigned organisation unit(s).
+     * @param mayApproveAtLowerLevels Tells whether the user has the authority
+     *        to approve data below the user's assigned organisation unit(s).
+     * @return true if the user may approve, otherwise false
+     */
+    private boolean isAuthorizedToUnapprove( OrganisationUnit source, User user,
+        boolean mayApproveAtSameLevel, boolean mayApproveAtLowerLevels )
+    {
+        if ( mayApprove( source, user, mayApproveAtSameLevel, mayApproveAtLowerLevels ) )
+        {
+            return true;
+        }
+
+        for ( OrganisationUnit ancestor : source.getAncestors() )
+        {
+            if ( mayApprove( ancestor, user, mayApproveAtSameLevel, mayApproveAtLowerLevels ) )
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}

=== added directory 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate'
=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalStore.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalStore.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalStore.java	2013-12-25 15:01:48 +0000
@@ -0,0 +1,90 @@
+package org.hisp.dhis.dataapproval.hibernate;
+
+/*
+ * Copyright (c) 2004-2013, 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.hibernate.Criteria;
+import org.hibernate.criterion.Restrictions;
+import org.hisp.dhis.dataapproval.DataApproval;
+import org.hisp.dhis.dataapproval.DataApprovalStore;
+import org.hisp.dhis.dataelement.DataElementCategoryOptionCombo;
+import org.hisp.dhis.dataset.DataSet;
+import org.hisp.dhis.hibernate.HibernateGenericStore;
+import org.hisp.dhis.organisationunit.OrganisationUnit;
+import org.hisp.dhis.period.Period;
+import org.hisp.dhis.period.PeriodService;
+
+/**
+ * @author Jim Grace
+ * @version $Id$
+ */
+public class HibernateDataApprovalStore
+    extends HibernateGenericStore<DataApproval>
+    implements DataApprovalStore
+{
+    // -------------------------------------------------------------------------
+    // Dependencies
+    // -------------------------------------------------------------------------
+
+    private PeriodService periodService;
+
+    public void setPeriodService( PeriodService periodService )
+    {
+        this.periodService = periodService;
+    }
+
+    // -------------------------------------------------------------------------
+    // DataApproval
+    // -------------------------------------------------------------------------
+
+    public void addDataApproval( DataApproval dataApproval )
+    {
+        dataApproval.setPeriod( periodService.reloadPeriod( dataApproval.getPeriod() ) );
+
+        save( dataApproval );
+    }
+
+    public void deleteDataApproval( DataApproval dataApproval )
+    {
+        delete( dataApproval );
+    }
+
+    public DataApproval getDataApproval( DataSet dataSet, Period period, 
+        OrganisationUnit organisationUnit, DataElementCategoryOptionCombo attributeOptionCombo )
+    {
+        Period storedPeriod = periodService.reloadPeriod( period );
+
+        Criteria criteria = getCriteria();
+        criteria.add( Restrictions.eq( "dataSet", dataSet ) );
+        criteria.add( Restrictions.eq( "period", storedPeriod ) );
+        criteria.add( Restrictions.eq( "organisationUnit", organisationUnit ) );
+        criteria.add( Restrictions.eq( "attributeOptionCombo", attributeOptionCombo ) );
+
+        return (DataApproval) criteria.uniqueResult();
+    }
+}

=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/startup/TableAlteror.java'
--- dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/startup/TableAlteror.java	2013-12-20 14:53:40 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/startup/TableAlteror.java	2013-12-22 21:08:30 +0000
@@ -410,6 +410,7 @@
         executeSql( "update dataset set allowfutureperiods = false where allowfutureperiods is null" );
         executeSql( "update dataset set validcompleteonly = false where validcompleteonly is null" );
         executeSql( "update dataset set notifycompletinguser = false where notifycompletinguser is null" );
+        executeSql( "update dataset set approvedata = false where approvedata is null" );
         executeSql( "update dataelement set zeroissignificant = false where zeroissignificant is null" );
         executeSql( "update organisationunit set haspatients = false where haspatients is null" );
         executeSql( "update dataset set expirydays = 0 where expirydays is null" );

=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/beans.xml'
--- dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/beans.xml	2013-12-23 09:13:02 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/resources/META-INF/dhis/beans.xml	2013-12-25 15:01:48 +0000
@@ -56,6 +56,13 @@
     <property name="jdbcTemplate" ref="jdbcTemplate" />
   </bean>
 
+  <bean id="org.hisp.dhis.dataapproval.DataApprovalStore" class="org.hisp.dhis.dataapproval.hibernate.HibernateDataApprovalStore">
+    <property name="clazz" value="org.hisp.dhis.dataapproval.DataApproval" />
+    <property name="sessionFactory" ref="sessionFactory" />
+    <property name="periodService" ref="org.hisp.dhis.period.PeriodService" />
+  </bean>
+
+    <!--@author Ovidiu Rosu <rosu.ovi@xxxxxxxxx>-->
   <bean id="org.hisp.dhis.filter.MetaDataFilterStore" class="org.hisp.dhis.metadatafilter.hibernate.HibernateMetaDataFilterStore">
     <property name="clazz" value="org.hisp.dhis.filter.MetaDataFilter" />
     <property name="sessionFactory" ref="sessionFactory" />
@@ -375,6 +382,10 @@
     <property name="dataValueAuditStore" ref="org.hisp.dhis.datavalue.DataValueAuditStore" />
   </bean>
 
+  <bean id="org.hisp.dhis.dataapproval.DataApprovalService" class="org.hisp.dhis.dataapproval.DefaultDataApprovalService">
+      <property name="dataApprovalStore" ref="org.hisp.dhis.dataapproval.DataApprovalStore" />
+  </bean>
+
   <bean id="org.hisp.dhis.dataelement.DataElementService" class="org.hisp.dhis.dataelement.DefaultDataElementService">
     <property name="dataElementStore" ref="org.hisp.dhis.dataelement.DataElementStore" />
     <property name="dataElementGroupStore" ref="org.hisp.dhis.dataelement.DataElementGroupStore" />

=== added directory 'dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataapproval'
=== added directory 'dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataapproval/hibernate'
=== added file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataapproval/hibernate/DataApproval.hbm.xml'
--- dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataapproval/hibernate/DataApproval.hbm.xml	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataapproval/hibernate/DataApproval.hbm.xml	2013-12-25 15:01:48 +0000
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!DOCTYPE hibernate-mapping PUBLIC
+  "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
+  "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd";
+  [<!ENTITY identifiableProperties SYSTEM "classpath://org/hisp/dhis/common/identifiableProperties.hbm">]
+  >
+<hibernate-mapping>
+  <class name="org.hisp.dhis.dataapproval.DataApproval" table="dataapproval">
+
+    <id name="id" column="dataapprovalid">
+      <generator class="native" />
+    </id>
+
+    <properties name="dataapproval_dataset_period_orgunit_unique_key" unique="true">
+      <many-to-one name="dataSet" class="org.hisp.dhis.dataset.DataSet" column="datasetid" foreign-key="fk_dataapproval_datasetid" />
+      <many-to-one name="period" class="org.hisp.dhis.period.Period" column="periodid" foreign-key="fk_dataapproval_periodid" />
+      <many-to-one name="organisationUnit" class="org.hisp.dhis.organisationunit.OrganisationUnit" column="organisationunitid" foreign-key="fk_dataapproval_organisationunitid" />
+      <many-to-one name="attributeOptionCombo" class="org.hisp.dhis.dataelement.DataElementCategoryOptionCombo" column="attributeoptioncomboid" foreign-key="fK_dataapproval_attributeoptioncomboid" />
+    </properties>
+
+    <property name="created" column="created" type="timestamp" />
+
+    <many-to-one name="creator" class="org.hisp.dhis.user.User" column="creator" foreign-key="fk_dataapproval_creator" />
+
+  </class>
+</hibernate-mapping>

=== modified file 'dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataset/hibernate/DataSet.hbm.xml'
--- dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataset/hibernate/DataSet.hbm.xml	2013-12-20 14:53:40 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataset/hibernate/DataSet.hbm.xml	2013-12-22 21:08:30 +0000
@@ -83,9 +83,11 @@
     <many-to-one name="notificationRecipients" class="org.hisp.dhis.user.UserGroup"
       foreign-key="fk_dataset_notificationrecipients" />
 
-    <property name="notifyCompletingUser" />
-
-    <!-- Form properties -->
+      <property name="notifyCompletingUser" />
+
+      <property name="approveData" />
+
+      <!-- Form properties -->
 
     <property name="allowFuturePeriods" />
 

=== added directory 'dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataapproval'
=== added 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	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataapproval/DataApprovalServiceTest.java	2013-12-25 15:01:48 +0000
@@ -0,0 +1,379 @@
+package org.hisp.dhis.dataapproval;
+
+/*
+ * Copyright (c) 2004-2013, 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import java.util.Date;
+
+import org.hisp.dhis.DhisSpringTest;
+import org.hisp.dhis.dataelement.DataElementCategoryOptionCombo;
+import org.hisp.dhis.dataelement.DataElementCategoryService;
+import org.hisp.dhis.dataset.DataSet;
+import org.hisp.dhis.dataset.DataSetService;
+import org.hisp.dhis.organisationunit.OrganisationUnit;
+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.user.User;
+import org.hisp.dhis.user.UserService;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * @author Jim Grace
+ * @version $Id$
+ */
+public class DataApprovalServiceTest
+        extends DhisSpringTest
+{
+    @Autowired
+    private DataApprovalService dataApprovalService;
+
+    @Autowired
+    private PeriodService periodService;
+
+    @Autowired
+    private DataElementCategoryService categoryService;
+
+    @Autowired
+    private DataSetService dataSetService;
+    
+    @Autowired
+    private UserService userService;
+
+    @Autowired
+    private OrganisationUnitService organisationUnitService;
+    
+    // -------------------------------------------------------------------------
+    // Supporting data
+    // -------------------------------------------------------------------------
+
+    private DataSet dataSetA;
+
+    private DataSet dataSetB;
+
+    private Period periodA;
+
+    private Period periodB;
+
+    private OrganisationUnit organisationUnitA;
+
+    private OrganisationUnit organisationUnitB;
+
+    private OrganisationUnit organisationUnitC;
+
+    private OrganisationUnit organisationUnitD;
+
+    private User userA;
+
+    private User userB;
+
+    private DataElementCategoryOptionCombo attributeOptionCombo;
+    
+    // -------------------------------------------------------------------------
+    // Set up/tear down
+    // -------------------------------------------------------------------------
+
+    @Override
+    public void setUpTest() throws Exception
+    {
+        // ---------------------------------------------------------------------
+        // Add supporting data
+        // ---------------------------------------------------------------------
+
+        PeriodType periodType = PeriodType.getPeriodTypeByName( "Monthly" );
+
+        dataSetA = createDataSet( 'A', periodType );
+        dataSetB = createDataSet( 'B', periodType );
+
+        dataSetService.addDataSet( dataSetA );
+        dataSetService.addDataSet( dataSetB );
+
+        periodA = createPeriod( getDay( 5 ), getDay( 6 ) );
+        periodB = createPeriod( getDay( 6 ), getDay( 7 ) );
+
+        periodService.addPeriod( periodA );
+        periodService.addPeriod( periodB );
+
+        organisationUnitA = createOrganisationUnit( 'A' );
+        organisationUnitB = createOrganisationUnit( 'B', organisationUnitA );
+        organisationUnitC = createOrganisationUnit( 'C', organisationUnitB );
+        organisationUnitD = createOrganisationUnit( 'D', organisationUnitC );
+
+        organisationUnitService.addOrganisationUnit( organisationUnitA );
+        organisationUnitService.addOrganisationUnit( organisationUnitB );
+        organisationUnitService.addOrganisationUnit( organisationUnitC );
+        organisationUnitService.addOrganisationUnit( organisationUnitD );
+
+        userA = createUser( 'A' );
+        userB = createUser( 'B' );
+
+        userService.addUser( userA );
+        userService.addUser( userB );
+        
+        attributeOptionCombo = categoryService.getDefaultDataElementCategoryOptionCombo();
+    }
+
+    // -------------------------------------------------------------------------
+    // Basic DataApproval
+    // -------------------------------------------------------------------------
+
+    @Test
+    public void testAddAndGetDataApproval() throws Exception
+    {
+        Date date = new Date();
+        DataApproval dataApprovalA = new DataApproval( dataSetA, periodA, organisationUnitA, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalB = new DataApproval( dataSetA, periodA, organisationUnitB, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalC = new DataApproval( dataSetA, periodB, organisationUnitA, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalD = new DataApproval( dataSetB, periodA, organisationUnitA, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalE;
+
+        dataApprovalService.addDataApproval( dataApprovalA );
+        dataApprovalService.addDataApproval( dataApprovalB );
+        dataApprovalService.addDataApproval( dataApprovalC );
+        dataApprovalService.addDataApproval( dataApprovalD );
+
+        dataApprovalA = dataApprovalService.getDataApproval( dataSetA, periodA, organisationUnitA, attributeOptionCombo );
+        assertNotNull( dataApprovalA );
+        assertEquals( dataSetA.getId(), dataApprovalA.getDataSet().getId() );
+        assertEquals( periodA, dataApprovalA.getPeriod() );
+        assertEquals( organisationUnitA.getId(), dataApprovalA.getOrganisationUnit().getId() );
+        assertEquals( date, dataApprovalA.getCreated() );
+        assertEquals( userA.getId(), dataApprovalA.getCreator().getId() );
+
+        dataApprovalB = dataApprovalService.getDataApproval( dataSetA, periodA, organisationUnitB, attributeOptionCombo );
+        assertNotNull( dataApprovalB );
+        assertEquals( dataSetA.getId(), dataApprovalB.getDataSet().getId() );
+        assertEquals( periodA, dataApprovalB.getPeriod() );
+        assertEquals( organisationUnitB.getId(), dataApprovalB.getOrganisationUnit().getId() );
+        assertEquals( date, dataApprovalB.getCreated() );
+        assertEquals( userA.getId(), dataApprovalB.getCreator().getId() );
+
+        dataApprovalC = dataApprovalService.getDataApproval( dataSetA, periodB, organisationUnitA, attributeOptionCombo );
+        assertNotNull( dataApprovalC );
+        assertEquals( dataSetA.getId(), dataApprovalC.getDataSet().getId() );
+        assertEquals( periodB, dataApprovalC.getPeriod() );
+        assertEquals( organisationUnitA.getId(), dataApprovalC.getOrganisationUnit().getId() );
+        assertEquals( date, dataApprovalC.getCreated() );
+        assertEquals( userA.getId(), dataApprovalC.getCreator().getId() );
+
+        dataApprovalD = dataApprovalService.getDataApproval( dataSetB, periodA, organisationUnitA, attributeOptionCombo );
+        assertNotNull( dataApprovalD );
+        assertEquals( dataSetB.getId(), dataApprovalD.getDataSet().getId() );
+        assertEquals( periodA, dataApprovalD.getPeriod() );
+        assertEquals( organisationUnitA.getId(), dataApprovalD.getOrganisationUnit().getId() );
+        assertEquals( date, dataApprovalD.getCreated() );
+        assertEquals( userA.getId(), dataApprovalD.getCreator().getId() );
+
+        dataApprovalE = dataApprovalService.getDataApproval( dataSetB, periodB, organisationUnitB, attributeOptionCombo );
+        assertNull( dataApprovalE );
+    }
+
+    @Test
+    public void testAddDuplicateDataApproval() throws Exception
+    {
+        Date date = new Date();
+        DataApproval dataApprovalA = new DataApproval( dataSetA, periodA, organisationUnitA, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalB = new DataApproval( dataSetA, periodA, organisationUnitA, attributeOptionCombo, date, userA );
+
+        dataApprovalService.addDataApproval( dataApprovalA );
+
+        try
+        {
+            dataApprovalService.addDataApproval( dataApprovalB );
+            fail("Should give unique constraint violation");
+        }
+        catch ( Exception e )
+        {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testDeleteDataApproval() throws Exception
+    {
+        Date date = new Date();
+        DataApproval dataApprovalA = new DataApproval( dataSetA, periodA, organisationUnitA, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalB = new DataApproval( dataSetA, periodA, organisationUnitB, attributeOptionCombo, date, userB );
+        DataApproval testA;
+        DataApproval testB;
+
+        dataApprovalService.addDataApproval( dataApprovalA );
+        dataApprovalService.addDataApproval( dataApprovalB );
+
+        testA = dataApprovalService.getDataApproval( dataSetA, periodA, organisationUnitA, attributeOptionCombo );
+        assertNotNull( testA );
+
+        testB = dataApprovalService.getDataApproval( dataSetA, periodA, organisationUnitB, attributeOptionCombo );
+        assertNotNull( testB );
+
+        dataApprovalService.deleteDataApproval( dataApprovalA ); // Only A should be deleted.
+
+        testA = dataApprovalService.getDataApproval( dataSetA, periodA, organisationUnitA, attributeOptionCombo );
+        assertNull( testA );
+
+        testB = dataApprovalService.getDataApproval( dataSetA, periodA, organisationUnitB, attributeOptionCombo );
+        assertNotNull( testB );
+
+        dataApprovalService.addDataApproval( dataApprovalA );
+        dataApprovalService.deleteDataApproval( dataApprovalB ); // A and B should both be deleted.
+
+        testA = dataApprovalService.getDataApproval( dataSetA, periodA, organisationUnitA, attributeOptionCombo );
+        assertNull( testA );
+
+        testB = dataApprovalService.getDataApproval( dataSetA, periodA, organisationUnitB, attributeOptionCombo );
+        assertNull( testB );
+    }
+
+    @Test
+    public void testGetDataApprovalState() throws Exception
+    {
+        // Not enabled.
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitA, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitB, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitC, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitD, attributeOptionCombo ) );
+
+        // Enabled for data set, but data set not associated with organisation unit.
+        dataSetA.setApproveData( true );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitA, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitB, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitC, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitD, attributeOptionCombo ) );
+
+        // Enabled for data set, and associated with organisation unit C.
+        organisationUnitC.addDataSet( dataSetA );
+        assertEquals( DataApprovalState.WAITING_FOR_LOWER_LEVEL_APPROVAL, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitA, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.WAITING_FOR_LOWER_LEVEL_APPROVAL, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitB, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.READY_FOR_APPROVAL, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitC, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitD, attributeOptionCombo ) );
+
+        // Approved for sourceC
+        Date date = new Date();
+        DataApproval dataApprovalA = new DataApproval( dataSetA, periodA, organisationUnitC, attributeOptionCombo, date, userA );
+        dataApprovalService.addDataApproval( dataApprovalA );
+        assertEquals( DataApprovalState.WAITING_FOR_LOWER_LEVEL_APPROVAL, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitA, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.READY_FOR_APPROVAL, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitB, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.APPROVED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitC, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitD, attributeOptionCombo ) );
+
+        // Disable approval for dataset.
+        dataSetA.setApproveData( false );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitA, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitB, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitC, attributeOptionCombo ) );
+        assertEquals( DataApprovalState.APPROVAL_NOT_NEEDED, dataApprovalService.getDataApprovalState( dataSetA, periodA, organisationUnitD, attributeOptionCombo ) );
+    }
+
+    @Test
+    public void testMayApprove() throws Exception
+    {
+        userB.addOrganisationUnit( organisationUnitB );
+
+        assertEquals( false, dataApprovalService.mayApprove( organisationUnitA, userB, false, false ) );
+        assertEquals( false, dataApprovalService.mayApprove( organisationUnitB, userB, false, false ) );
+        assertEquals( false, dataApprovalService.mayApprove( organisationUnitC, userB, false, false ) );
+        assertEquals( false, dataApprovalService.mayApprove( organisationUnitD, userB, false, false ) );
+
+        assertEquals( false, dataApprovalService.mayApprove( organisationUnitA, userB, false, true ) );
+        assertEquals( false, dataApprovalService.mayApprove( organisationUnitB, userB, false, true ) );
+        assertEquals( true, dataApprovalService.mayApprove( organisationUnitC, userB, false, true ) );
+        assertEquals( true, dataApprovalService.mayApprove( organisationUnitD, userB, false, true ) );
+
+        assertEquals( false, dataApprovalService.mayApprove( organisationUnitA, userB, true, false ) );
+        assertEquals( true, dataApprovalService.mayApprove( organisationUnitB, userB, true, false ) );
+        assertEquals( false, dataApprovalService.mayApprove( organisationUnitC, userB, true, false ) );
+        assertEquals( false, dataApprovalService.mayApprove( organisationUnitD, userB, true, false ) );
+
+        assertEquals( false, dataApprovalService.mayApprove( organisationUnitA, userB, true, true ) );
+        assertEquals( true, dataApprovalService.mayApprove( organisationUnitB, userB, true, true ) );
+        assertEquals( true, dataApprovalService.mayApprove( organisationUnitC, userB, true, true ) );
+        assertEquals( true, dataApprovalService.mayApprove( organisationUnitD, userB, true, true ) );
+    }
+
+    @Test
+    public void testMayUnapprove() throws Exception
+    {
+        userA.addOrganisationUnit( organisationUnitA );
+        userB.addOrganisationUnit( organisationUnitB );
+
+        Date date = new Date();
+        DataApproval dataApprovalA = new DataApproval( dataSetA, periodA, organisationUnitA, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalB = new DataApproval( dataSetA, periodA, organisationUnitB, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalC = new DataApproval( dataSetA, periodA, organisationUnitC, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalD = new DataApproval( dataSetA, periodA, organisationUnitD, attributeOptionCombo, date, userA );
+
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalA, userB, false, false ) );
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalB, userB, false, false ) );
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalC, userB, false, false ) );
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalD, userB, false, false ) );
+
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalA, userB, false, true ) );
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalB, userB, false, true ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalC, userB, false, true ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalD, userB, false, true ) );
+
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalA, userB, true, false ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalB, userB, true, false ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalC, userB, true, false ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalD, userB, true, false ) );
+
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalA, userB, true, true ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalB, userB, true, true ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalC, userB, true, true ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalD, userB, true, true ) );
+
+        // If the organisation unit has no parent:
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalA, userA, false, false ) );
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalA, userA, false, true ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalA, userA, true, false ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalA, userA, true, true ) );
+
+        dataApprovalService.addDataApproval( dataApprovalB );
+        dataApprovalService.addDataApproval( dataApprovalC );
+        dataApprovalService.addDataApproval( dataApprovalD );
+
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalA, userB, true, true ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalB, userB, true, true ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalC, userB, true, true ) );
+        assertEquals( true, dataApprovalService.mayUnapprove( dataApprovalD, userB, true, true ) );
+
+        dataApprovalService.addDataApproval( dataApprovalA );
+
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalA, userB, true, true ) );
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalB, userB, true, true ) );
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalC, userB, true, true ) );
+        assertEquals( false, dataApprovalService.mayUnapprove( dataApprovalD, userB, true, true ) );
+    }
+}

=== added file 'dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataapproval/DataApprovalStoreTest.java'
--- dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataapproval/DataApprovalStoreTest.java	1970-01-01 00:00:00 +0000
+++ dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataapproval/DataApprovalStoreTest.java	2013-12-25 15:01:48 +0000
@@ -0,0 +1,255 @@
+package org.hisp.dhis.dataapproval;
+
+/*
+ * Copyright (c) 2004-2013, 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import java.util.Date;
+
+import org.hisp.dhis.DhisSpringTest;
+import org.hisp.dhis.dataelement.DataElementCategoryOptionCombo;
+import org.hisp.dhis.dataelement.DataElementCategoryService;
+import org.hisp.dhis.dataset.DataSet;
+import org.hisp.dhis.dataset.DataSetService;
+import org.hisp.dhis.organisationunit.OrganisationUnit;
+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.user.User;
+import org.hisp.dhis.user.UserService;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * @author Jim Grace
+ * @version $Id$
+ */
+public class DataApprovalStoreTest
+    extends DhisSpringTest
+{
+    @Autowired
+    private DataApprovalStore dataApprovalStore;
+
+    @Autowired
+    private PeriodService periodService;
+
+    @Autowired
+    private DataElementCategoryService categoryService;
+
+    @Autowired
+    private DataSetService dataSetService;
+    
+    @Autowired
+    private UserService userService;
+
+    @Autowired
+    private OrganisationUnitService organisationUnitService;
+    
+    // -------------------------------------------------------------------------
+    // Supporting data
+    // -------------------------------------------------------------------------
+
+    private DataSet dataSetA;
+
+    private DataSet dataSetB;
+
+    private Period periodA;
+
+    private Period periodB;
+
+    private OrganisationUnit sourceA;
+
+    private OrganisationUnit sourceB;
+
+    private OrganisationUnit sourceC;
+
+    private OrganisationUnit sourceD;
+
+    private User userA;
+
+    private User userB;
+
+    private DataElementCategoryOptionCombo attributeOptionCombo;
+    
+    // -------------------------------------------------------------------------
+    // Set up/tear down
+    // -------------------------------------------------------------------------
+
+    @Override
+    public void setUpTest() throws Exception
+    {
+        // ---------------------------------------------------------------------
+        // Add supporting data
+        // ---------------------------------------------------------------------
+
+        PeriodType periodType = PeriodType.getPeriodTypeByName( "Monthly" );
+
+        dataSetA = createDataSet( 'A', periodType );
+        dataSetB = createDataSet( 'B', periodType );
+
+        dataSetService.addDataSet( dataSetA );
+        dataSetService.addDataSet( dataSetB );
+
+        periodA = createPeriod( getDay( 5 ), getDay( 6 ) );
+        periodB = createPeriod( getDay( 6 ), getDay( 7 ) );
+
+        periodService.addPeriod( periodA );
+        periodService.addPeriod( periodB );
+
+        sourceA = createOrganisationUnit( 'A' );
+        sourceB = createOrganisationUnit( 'B', sourceA );
+        sourceC = createOrganisationUnit( 'C', sourceB );
+        sourceD = createOrganisationUnit( 'D', sourceC );
+
+        organisationUnitService.addOrganisationUnit( sourceA );
+        organisationUnitService.addOrganisationUnit( sourceB );
+        organisationUnitService.addOrganisationUnit( sourceC );
+        organisationUnitService.addOrganisationUnit( sourceD );
+
+        userA = createUser( 'A' );
+        userB = createUser( 'B' );
+
+        userService.addUser( userA );
+        userService.addUser( userB );
+
+        attributeOptionCombo = categoryService.getDefaultDataElementCategoryOptionCombo();
+    }
+
+    // -------------------------------------------------------------------------
+    // Basic DataApproval
+    // -------------------------------------------------------------------------
+
+    @Test
+    public void testAddAndGetDataApproval() throws Exception
+    {
+        Date date = new Date();
+        DataApproval dataApprovalA = new DataApproval( dataSetA, periodA, sourceA, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalB = new DataApproval( dataSetA, periodA, sourceB, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalC = new DataApproval( dataSetA, periodB, sourceA, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalD = new DataApproval( dataSetB, periodA, sourceA, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalE;
+
+        dataApprovalStore.addDataApproval( dataApprovalA );
+        dataApprovalStore.addDataApproval( dataApprovalB );
+        dataApprovalStore.addDataApproval( dataApprovalC );
+        dataApprovalStore.addDataApproval( dataApprovalD );
+
+        dataApprovalA = dataApprovalStore.getDataApproval( dataSetA, periodA, sourceA, attributeOptionCombo );
+        assertNotNull( dataApprovalA );
+        assertEquals( dataSetA.getId(), dataApprovalA.getDataSet().getId() );
+        assertEquals( periodA, dataApprovalA.getPeriod() );
+        assertEquals( sourceA.getId(), dataApprovalA.getOrganisationUnit().getId() );
+        assertEquals( date, dataApprovalA.getCreated() );
+        assertEquals( userA.getId(), dataApprovalA.getCreator().getId() );
+
+        dataApprovalB = dataApprovalStore.getDataApproval( dataSetA, periodA, sourceB, attributeOptionCombo );
+        assertNotNull( dataApprovalB );
+        assertEquals( dataSetA.getId(), dataApprovalB.getDataSet().getId() );
+        assertEquals( periodA, dataApprovalB.getPeriod() );
+        assertEquals( sourceB.getId(), dataApprovalB.getOrganisationUnit().getId() );
+        assertEquals( date, dataApprovalB.getCreated() );
+        assertEquals( userA.getId(), dataApprovalB.getCreator().getId() );
+
+        dataApprovalC = dataApprovalStore.getDataApproval( dataSetA, periodB, sourceA, attributeOptionCombo );
+        assertNotNull( dataApprovalC );
+        assertEquals( dataSetA.getId(), dataApprovalC.getDataSet().getId() );
+        assertEquals( periodB, dataApprovalC.getPeriod() );
+        assertEquals( sourceA.getId(), dataApprovalC.getOrganisationUnit().getId() );
+        assertEquals( date, dataApprovalC.getCreated() );
+        assertEquals( userA.getId(), dataApprovalC.getCreator().getId() );
+
+        dataApprovalD = dataApprovalStore.getDataApproval( dataSetB, periodA, sourceA, attributeOptionCombo );
+        assertNotNull( dataApprovalD );
+        assertEquals( dataSetB.getId(), dataApprovalD.getDataSet().getId() );
+        assertEquals( periodA, dataApprovalD.getPeriod() );
+        assertEquals( sourceA.getId(), dataApprovalD.getOrganisationUnit().getId() );
+        assertEquals( date, dataApprovalD.getCreated() );
+        assertEquals( userA.getId(), dataApprovalD.getCreator().getId() );
+
+        dataApprovalE = dataApprovalStore.getDataApproval( dataSetB, periodB, sourceB, attributeOptionCombo );
+        assertNull( dataApprovalE );
+    }
+
+    @Test
+    public void testAddDuplicateDataApproval() throws Exception
+    {
+        Date date = new Date();
+        DataApproval dataApprovalA = new DataApproval( dataSetA, periodA, sourceA, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalB = new DataApproval( dataSetA, periodA, sourceA, attributeOptionCombo, date, userA );
+
+        dataApprovalStore.addDataApproval( dataApprovalA );
+
+        try
+        {
+            dataApprovalStore.addDataApproval( dataApprovalB );
+            fail("Should give unique constraint violation");
+        }
+        catch ( Exception e )
+        {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testDeleteDataApproval() throws Exception
+    {
+        Date date = new Date();
+        DataApproval dataApprovalA = new DataApproval( dataSetA, periodA, sourceA, attributeOptionCombo, date, userA );
+        DataApproval dataApprovalB = new DataApproval( dataSetB, periodB, sourceB, attributeOptionCombo, date, userB );
+
+        dataApprovalStore.addDataApproval( dataApprovalA );
+        dataApprovalStore.addDataApproval( dataApprovalB );
+
+        dataApprovalA = dataApprovalStore.getDataApproval( dataSetA, periodA, sourceA, attributeOptionCombo );
+        assertNotNull( dataApprovalA );
+
+        dataApprovalB = dataApprovalStore.getDataApproval( dataSetB, periodB, sourceB, attributeOptionCombo );
+        assertNotNull( dataApprovalB );
+
+        dataApprovalStore.deleteDataApproval( dataApprovalA );
+
+        dataApprovalA = dataApprovalStore.getDataApproval( dataSetA, periodA, sourceA, attributeOptionCombo );
+        assertNull( dataApprovalA );
+
+        dataApprovalB = dataApprovalStore.getDataApproval( dataSetB, periodB, sourceB, attributeOptionCombo );
+        assertNotNull( dataApprovalB );
+
+        dataApprovalStore.deleteDataApproval( dataApprovalB );
+
+        dataApprovalA = dataApprovalStore.getDataApproval( dataSetA, periodA, sourceA, attributeOptionCombo );
+        assertNull( dataApprovalA );
+
+        dataApprovalB = dataApprovalStore.getDataApproval( dataSetB, periodB, sourceB, attributeOptionCombo );
+        assertNull( dataApprovalB );
+    }
+}

=== modified file 'dhis-2/dhis-support/dhis-support-jdbc/src/main/java/org/hisp/dhis/jdbc/batchhandler/DataSetBatchHandler.java'
--- dhis-2/dhis-support/dhis-support-jdbc/src/main/java/org/hisp/dhis/jdbc/batchhandler/DataSetBatchHandler.java	2013-09-30 11:02:47 +0000
+++ dhis-2/dhis-support/dhis-support-jdbc/src/main/java/org/hisp/dhis/jdbc/batchhandler/DataSetBatchHandler.java	2013-12-21 01:36:12 +0000
@@ -103,6 +103,7 @@
         statementBuilder.setColumn( "expirydays" );
         statementBuilder.setColumn( "timelydays" );
         statementBuilder.setColumn( "notifycompletinguser" );
+        statementBuilder.setColumn( "approvedata" );
         statementBuilder.setColumn( "skipaggregation" );
         statementBuilder.setColumn( "fieldcombinationrequired" );
         statementBuilder.setColumn( "validcompleteonly" );
@@ -126,6 +127,7 @@
         statementBuilder.setValue( dataSet.getExpiryDays() );
         statementBuilder.setValue( dataSet.getTimelyDays() );
         statementBuilder.setValue( dataSet.isNotifyCompletingUser() );
+        statementBuilder.setValue( dataSet.isApproveData() );
         statementBuilder.setValue( dataSet.isSkipAggregation() );
         statementBuilder.setValue( dataSet.isFieldCombinationRequired() );
         statementBuilder.setValue( dataSet.isValidCompleteOnly() );

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/java/org/hisp/dhis/dataset/action/AddDataSetAction.java'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/java/org/hisp/dhis/dataset/action/AddDataSetAction.java	2013-12-20 14:53:40 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/java/org/hisp/dhis/dataset/action/AddDataSetAction.java	2013-12-22 21:08:30 +0000
@@ -168,6 +168,13 @@
         this.notifyCompletingUser = notifyCompletingUser;
     }
 
+    private boolean approveData;
+
+    public void setApproveData( boolean approveData )
+    {
+        this.approveData = approveData;
+    }
+
     private boolean skipAggregation;
 
     public void setSkipAggregation( boolean skipAggregation )
@@ -310,6 +317,7 @@
         dataSet.setFieldCombinationRequired( fieldCombinationRequired );
         dataSet.setValidCompleteOnly( validCompleteOnly );
         dataSet.setNotifyCompletingUser( notifyCompletingUser );
+        dataSet.setApproveData( approveData );
         dataSet.setSkipOffline( skipOffline );
         dataSet.setDataElementDecoration( dataElementDecoration );
         dataSet.setRenderAsTabs( renderAsTabs );

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/java/org/hisp/dhis/dataset/action/UpdateDataSetAction.java'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/java/org/hisp/dhis/dataset/action/UpdateDataSetAction.java	2013-12-20 14:53:40 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/java/org/hisp/dhis/dataset/action/UpdateDataSetAction.java	2013-12-22 21:08:30 +0000
@@ -178,6 +178,13 @@
         this.notifyCompletingUser = notifyCompletingUser;
     }
 
+    private boolean approveData;
+
+    public void setApproveData( boolean approveData )
+    {
+        this.approveData = approveData;
+    }
+
     private boolean skipAggregation;
 
     public void setSkipAggregation( boolean skipAggregation )
@@ -335,6 +342,7 @@
         dataSet.setFieldCombinationRequired( fieldCombinationRequired );
         dataSet.setValidCompleteOnly( validCompleteOnly );
         dataSet.setNotifyCompletingUser( notifyCompletingUser );
+        dataSet.setApproveData( approveData );
         dataSet.setSkipOffline( skipOffline );
         dataSet.setDataElementDecoration( dataElementDecoration );
         dataSet.setRenderAsTabs( renderAsTabs );

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/resources/org/hisp/dhis/dataset/i18n_module.properties'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/resources/org/hisp/dhis/dataset/i18n_module.properties	2013-12-20 14:53:40 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/resources/org/hisp/dhis/dataset/i18n_module.properties	2013-12-22 21:08:30 +0000
@@ -106,6 +106,7 @@
 object_not_deleted_associated_by_objects=Object not deleted because it is associated by objects of type
 auto_save_data_entry_forms=Auto-save data entry forms
 notify_completing_user=Send notification to completing user
+approve_data=Approve data
 insert_images=Insert images
 dataelementdecoration=Data element decoration
 pdf_data_entry_form=Get PDF for Data Entry

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/webapp/dhis-web-maintenance-dataset/addDataSet.vm'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/webapp/dhis-web-maintenance-dataset/addDataSet.vm	2013-12-20 14:53:40 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/webapp/dhis-web-maintenance-dataset/addDataSet.vm	2013-12-22 21:08:30 +0000
@@ -126,6 +126,15 @@
         </td>
       </tr>
       <tr>
+          <td><label>$i18n.getString( "approve_data" )</label></td>
+          <td>
+              <select id="approveData" name="approveData">
+                  <option value="false">$i18n.getString( "no" )</option>
+                  <option value="true">$i18n.getString( "yes" )</option>
+              </select>
+          </td>
+      </tr>
+      <tr>
       	<td><label>$i18n.getString( "skip_aggregation" )</label></td>
       	<td>
       	  <select id="skipAggregation" name="skipAggregation">

=== modified file 'dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/webapp/dhis-web-maintenance-dataset/editDataSet.vm'
--- dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/webapp/dhis-web-maintenance-dataset/editDataSet.vm	2013-12-20 14:53:40 +0000
+++ dhis-2/dhis-web/dhis-web-maintenance/dhis-web-maintenance-dataset/src/main/webapp/dhis-web-maintenance-dataset/editDataSet.vm	2013-12-22 21:08:30 +0000
@@ -126,6 +126,15 @@
         </td>
       </tr>
       <tr>
+          <td><label>$i18n.getString( "approve_data" )</label></td>
+          <td>
+              <select id="approveData" name="approveData">
+                  <option value="false">$i18n.getString( "no" )</option>
+                  <option value="true"#if( $dataSet.approveData == true ) selected="selected"#end>$i18n.getString( "yes" )</option>
+              </select>
+          </td>
+      </tr>
+      <tr>
         <td><label>$i18n.getString( "skip_aggregation" )</label></td>
         <td>
           <select id="skipAggregation" name="skipAggregation">