=== 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. + *

+ * 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. + *

+ * 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. + *

+ * 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: + *

+ */ + 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 +{ + 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. + *

+ * 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 + 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 @@ + + + + + + + @@ -375,6 +382,10 @@ + + + + === 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 @@ + +] + > + + + + + + + + + + + + + + + + + + + + === 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 @@ - - - + + + + + === 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 @@ + + + + + + + + + + + +