Changeset 633


Ignore:
Timestamp:
07/19/10 08:47:38 (8 years ago)
Author:
gav
Message:

Domain change: Add PurchasingGroup?.
Logic and views to suite.

Location:
trunk/grails-app
Files:
7 added
19 edited

Legend:

Unmodified
Added
Removed
  • trunk/grails-app/conf/Config.groovy

    r627 r633  
    401401            [order:91, controller:'inventoryItemPurchaseDetailed', title:'Edit', action:'edit', isVisible: { params.action == 'edit' }] 
    402402        ] 
     403    ], 
     404    [order:230, controller:'purchasingGroupDetailed', title:'purchasingGroup', action:'list', 
     405        subItems: [ 
     406            [order:10, controller:'purchasingGroupDetailed', title:'Purchasing Group List', action:'list', isVisible: { true }], 
     407            [order:20, controller:'purchasingGroupDetailed', title:'Create', action:'create', isVisible: { true }], 
     408            [order:90, controller:'purchasingGroupDetailed', title:'Show', action:'show', isVisible: { params.action == 'show' }], 
     409            [order:91, controller:'purchasingGroupDetailed', title:'Edit', action:'edit', isVisible: { params.action == 'edit' }] 
     410        ] 
    403411    ] 
    404412] 
  • trunk/grails-app/controllers/CostCodeDetailedController.groovy

    r632 r633  
    7878            } 
    7979            costCodeInstance.properties = params 
     80            // Trim name to avoid spaces. 
     81            costCodeInstance.name = costCodeInstance.name.trim() 
    8082            if(!costCodeInstance.hasErrors() && costCodeInstance.save(flush: true)) { 
    8183                flash.message = "CostCode ${params.id} updated" 
     
    100102    def save = { 
    101103        def costCodeInstance = new CostCode(params) 
     104        // Trim name to avoid spaces. 
     105        costCodeInstance.name = costCodeInstance.name.trim() 
    102106        if(!costCodeInstance.hasErrors() && costCodeInstance.save(flush: true)) { 
    103107            flash.message = "CostCode ${costCodeInstance.id} created" 
  • trunk/grails-app/controllers/InventoryItemPurchaseDetailedController.groovy

    r615 r633  
    226226        params.returnTo = params.returnTo ?: 'inventoryItem' 
    227227 
    228         if(!result.error) 
    229             return [ inventoryItemPurchaseInstance : result.inventoryItemPurchaseInstance ] 
     228        def costCodes = [] 
     229 
     230        if(!result.error) { 
     231            if(inventoryPurchaseService.isPersonInPurchasingGroup(result.inventoryItemPurchaseInstance.costCode.purchasingGroup)) 
     232                costCodes = inventoryPurchaseService.getCostCodesByPerson() 
     233 
     234            return [ inventoryItemPurchaseInstance : result.inventoryItemPurchaseInstance, 
     235                            'costCodes': costCodes ] 
     236        } 
    230237 
    231238        flash.errorMessage = g.message(code: result.error.code, args: result.error.args) 
     
    248255        } 
    249256 
    250         render(view:'edit', model:[inventoryItemPurchaseInstance: result.inventoryItemPurchaseInstance.attach()]) 
     257        result.inventoryItemPurchaseInstance.attach() 
     258        result.inventoryItemPurchaseInstance.costCode.attach() 
     259        result.inventoryItemPurchaseInstance.costCode.purchasingGroup.attach() 
     260 
     261        def costCodes = [] 
     262        if(inventoryPurchaseService.isPersonInPurchasingGroup(result.inventoryItemPurchaseInstance.costCode.purchasingGroup)) 
     263            costCodes = inventoryPurchaseService.getCostCodesByPerson() 
     264 
     265        render(view:'edit', model:[inventoryItemPurchaseInstance: result.inventoryItemPurchaseInstance, 
     266                                                'costCodes': costCodes]) 
    251267    } 
    252268 
     
    262278        } 
    263279 
    264         return ['inventoryItemPurchaseInstance':inventoryItemPurchaseInstance] 
     280        def costCodes = inventoryPurchaseService.getCostCodesByPerson() 
     281 
     282        return ['inventoryItemPurchaseInstance': inventoryItemPurchaseInstance, 
     283                        'costCodes': costCodes] 
    265284    } 
    266285 
     
    282301        } 
    283302 
     303        def costCodes = inventoryPurchaseService.getCostCodesByPerson() 
     304 
    284305        params.errorMessage = g.message(code: result.error.code, args: result.error.args) 
    285         render(view:'create', model:['inventoryItemPurchaseInstance': result.inventoryItemPurchaseInstance]) 
     306        render(view:'create', model:['inventoryItemPurchaseInstance': result.inventoryItemPurchaseInstance, 
     307                                                    'costCodes': costCodes]) 
    286308    } 
    287309 
  • trunk/grails-app/controllers/PersonController.groovy

    r628 r633  
    157157            person.properties = params 
    158158            person.setPersonGroupsFromCheckBoxList(params.personGroups) 
     159            person.setPurchasingGroupsFromCheckBoxList(params.purchasingGroups) 
    159160 
    160161            if(params.pass == "") { 
     
    194195            person.password = authenticateService.encodePassword(params.pass) 
    195196            person.setPersonGroupsFromCheckBoxList(params.personGroups) 
     197            person.setPurchasingGroupsFromCheckBoxList(params.purchasingGroups) 
    196198            if (person.save(flush: true)) { 
    197199                addRemoveAuthorities(person) 
  • trunk/grails-app/domain/CostCode.groovy

    r441 r633  
    11class CostCode { 
     2 
     3    PurchasingGroup purchasingGroup 
     4 
    25    String name 
    36    String description = "" 
     
    710 
    811    static constraints = { 
    9         name(maxSize:50,unique:true,blank:false) 
     12        name(blank:false, maxSize:50, validator: {val, obj -> 
     13            // Name must be unique for a purchasingGroup. 
     14            def list = CostCode.withCriteria { 
     15                eq('purchasingGroup', obj.purchasingGroup) 
     16                eq('name', obj.name) 
     17                if(obj.id) 
     18                    notEqual('id', obj.id) 
     19            } 
     20            if(list.size() > 0) 
     21                return 'not.unique.for.purchasing.group' 
     22            // Success. 
     23            return true 
     24        }) 
    1025        description(maxSize:100) 
    1126    } 
    1227 
    1328    String toString() { 
    14         "${this.name}" 
     29        "${this.name}.${this.purchasingGroup}" 
    1530    } 
     31 
    1632} 
  • trunk/grails-app/domain/Person.groovy

    r402 r633  
    77                        tasks: Task, 
    88                        contacts: Contact, 
    9                         addresses: Address] 
     9                        addresses: Address, 
     10                        purchasingGroups: PurchasingGroup] 
    1011 
    1112    static belongsTo = [Authority] 
     
    6869    } 
    6970 
     71    //  This additional setter is used to convert the checkBoxList string or string array 
     72    //  of ids selected to the corresponding domain objects. 
     73    public void setPurchasingGroupsFromCheckBoxList(ids) { 
     74        def idList = [] 
     75        if(ids instanceof String) { 
     76                if(ids.isInteger()) 
     77                    idList << ids.toInteger() 
     78        } 
     79        else { 
     80            ids.each() { 
     81                if(it.isInteger()) 
     82                    idList << it.toInteger() 
     83            } 
     84        } 
     85        this.purchasingGroups = idList.collect { PurchasingGroup.get( it ) } 
     86    } 
     87 
    7088} // end class 
  • trunk/grails-app/i18n/messages.properties

    r631 r633  
    4545person.pass.doesNotMatch=Passwords must match 
    4646 
     47costCode.name.not.unique.for.purchasing.group=CostCode name must be unique for purchasingGroup. 
     48 
    4749# 
    4850# Help Balloon and property definitions. 
     
    6163may also provide a record of persons qualified or trained in a specific area. \ 
    6264    Groups provide no application authorisations. 
     65person.purchasingGroups=Purchasing Groups 
     66person.purchasingGroups.help=Purchasing groups determine the available cost codes that a person may purchase against. 
    6367person.loginName=Login Name 
    6468person.loginName.help=This is the id or name that the person will use to login to the application. 
     
    231235inventoryItemPurchase.delete.failure.payment.approved=Could not delete, payment has been approved. 
    232236inventoryItemPurchase.operation.not.permitted.on.inactive.or.obsolete.item=This operation is not permitted on an inactive or obsolete inventory item. 
     237inventoryItemPurchase.costCodes.not.found=No cost codes found, a person needs to be assigned to a purchasing group that has cost codes. 
    233238 
    234239assignedGroup.task.not.found=Please select a task and then ''Add Assigned Group''. 
  • trunk/grails-app/services/CreateDataService.groovy

    r622 r633  
    115115 
    116116        // Person and Utils 
    117         createDemoPersons() 
    118117        createDemoSites() 
    119118        createDemoDepartments() 
     
    121120        createDemoManufacturers() 
    122121        createDemoProductionReference() 
     122        createDemoPurchasingGroups()  /// @todo: Perhaps a 'createQuickStartData' method? 
    123123        createDemoCostCodes() 
     124        createDemoPersons() 
    124125 
    125126        // Assets 
     
    307308        personInstance.addToAuthorities(Authority.get(2)) // ROLE_Manager. 
    308309        personInstance.addToAuthorities(Authority.get(3)) // ROLE_AppUser. 
     310        personInstance.addToPersonGroups(PersonGroup.get(1)) 
     311        personInstance.addToPurchasingGroups(PurchasingGroup.get(1)) 
     312        personInstance.addToPurchasingGroups(PurchasingGroup.get(2)) 
    309313 
    310314        //Person #4 
     
    697701    } 
    698702 
     703    void createDemoPurchasingGroups() { 
     704 
     705        // PurchasingGroup 
     706        def purchasingGroupInstance 
     707 
     708        purchasingGroupInstance = new PurchasingGroup(name:"R&M") 
     709        saveAndTest(purchasingGroupInstance) 
     710 
     711        purchasingGroupInstance = new PurchasingGroup(name:"Raw Materials") 
     712        saveAndTest(purchasingGroupInstance) 
     713 
     714        purchasingGroupInstance = new PurchasingGroup(name:"Safety") 
     715        saveAndTest(purchasingGroupInstance) 
     716    } 
     717 
    699718    def createDemoCostCodes() { 
    700719 
     
    703722 
    704723        // CostCode #1 
    705         costCodeInstance = new CostCode(name: "RM Reelstand") 
     724        costCodeInstance = new CostCode(name: "Reelstand.172", 
     725                                                                    purchasingGroup: PurchasingGroup.get(1)) 
    706726        saveAndTest(costCodeInstance) 
    707727 
    708728        // CostCode #2 
    709         costCodeInstance = new CostCode(name: "CAPEX Reelstand") 
     729        costCodeInstance = new CostCode(name: "Reelstand.CAPEX", 
     730                                                                    purchasingGroup: PurchasingGroup.get(1)) 
     731        saveAndTest(costCodeInstance) 
     732 
     733        // CostCode #2 
     734        costCodeInstance = new CostCode(name: "PrintUnit.123", 
     735                                                                    purchasingGroup: PurchasingGroup.get(3)) 
    710736        saveAndTest(costCodeInstance) 
    711737    } 
  • trunk/grails-app/services/InventoryPurchaseService.groovy

    r610 r633  
    8989    } 
    9090 
     91    /** 
     92    * Get costCodes by person and the purchasingGroups they have been assigned. 
     93    * @param person A Person, defaults to currentUser. 
     94    * @returns A list of CostCodes. 
     95    */ 
     96    def getCostCodesByPerson(person = authService.currentUser) { 
     97        if(person.purchasingGroups) { 
     98            CostCode.withCriteria { 
     99                    eq('isActive', true) 
     100                    or { 
     101                        person.purchasingGroups.each() { purchasingGroup -> 
     102                            eq('purchasingGroup', purchasingGroup) 
     103                        } 
     104                    } 
     105            }.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) } // withCriteria 
     106        } 
     107        else 
     108            [] 
     109    } // getCostCodesByPerson 
     110 
     111    /** 
     112    * Check if a person is in a purchasing group. 
     113    * @param person A PurchasingGroup to check for. 
     114    * @param person A Person, defaults to currentUser. 
     115    * @returns True if person is in group. 
     116    */ 
     117    def isPersonInPurchasingGroup(purchasingGroup, person = authService.currentUser) { 
     118        for(pg in person.purchasingGroups) { 
     119            if(pg.id == purchasingGroup.id) 
     120                return true 
     121        } 
     122    } // isPersonInPurchasingGroup 
     123 
    91124    def delete(params) { 
    92125        InventoryItemPurchase.withTransaction { status -> 
  • trunk/grails-app/views/appCore/manager.gsp

    r624 r633  
    2727                                <a href="${createLink(controller:'personGroupDetailed', action:'list')}">Assigned Groups</a> 
    2828                                <br /> 
     29                                <a href="${createLink(controller:'costCodeDetailed', action:'list')}">Cost Codes</a> 
     30                                <br /> 
    2931                                <a href="${createLink(controller:'departmentDetailed', action:'list')}">Departments</a> 
    3032                                <br /> 
     
    3840                                <br /> 
    3941                                <a href="${createLink(controller:'manufacturerDetailed', action:'list')}">Manufacturers</a> 
     42                                <br /> 
     43                                <a href="${createLink(controller:'purchasingGroupDetailed', action:'list')}">Purchasing Groups</a> 
    4044                                <br /> 
    4145                                <a href="${createLink(controller:'productionReferenceDetailed', action:'list')}">Production Reference</a> 
  • trunk/grails-app/views/costCodeDetailed/create.gsp

    r441 r633  
    3535                            <tr class="prop"> 
    3636                                <td valign="top" class="name"> 
     37                                    <label for="purchasingGroup">Purchasing Group:</label> 
     38                                </td> 
     39                                <td valign="top" class="value ${hasErrors(bean:costCodeInstance,field:'purchasingGroup','errors')}"> 
     40                                    <g:select optionKey="id" 
     41                                                    from="${PurchasingGroup.findAllByIsActive(true).sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }}" 
     42                                                    name="purchasingGroup.id" 
     43                                                    value="${costCodeInstance?.purchasingGroup?.id}" > 
     44                                    </g:select> 
     45                                    <p> 
     46                                        <g:link controller="purchasingGroupDetailed" action="create">+Add Group</g:link> 
     47                                    </p> 
     48                                </td> 
     49                            </tr>  
     50                         
     51                            <tr class="prop"> 
     52                                <td valign="top" class="name"> 
    3753                                    <label for="description">Description:</label> 
    3854                                </td> 
  • trunk/grails-app/views/costCodeDetailed/edit.gsp

    r441 r633  
    3737                            <tr class="prop"> 
    3838                                <td valign="top" class="name"> 
     39                                    <label for="purchasingGroup">Purchasing Group:</label> 
     40                                </td> 
     41                                <td valign="top" class="value ${hasErrors(bean:costCodeInstance,field:'purchasingGroup','errors')}"> 
     42                                    <g:select optionKey="id" 
     43                                                    from="${PurchasingGroup.findAllByIsActive(true).sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) }}" 
     44                                                    name="purchasingGroup.id" 
     45                                                    value="${costCodeInstance?.purchasingGroup?.id}" > 
     46                                    </g:select> 
     47                                    <p> 
     48                                        <g:link controller="purchasingGroupDetailed" action="create">+Add Group</g:link> 
     49                                    </p> 
     50                                </td> 
     51                            </tr>  
     52                         
     53                            <tr class="prop"> 
     54                                <td valign="top" class="name"> 
    3955                                    <label for="description">Description:</label> 
    4056                                </td> 
  • trunk/grails-app/views/costCodeDetailed/list.gsp

    r498 r633  
    2323                                <g:sortableColumn property="name" title="Name" /> 
    2424                         
     25                            <g:sortableColumn property="purchasingGroup" title="Group" /> 
     26                         
    2527                                <g:sortableColumn property="description" title="Description" /> 
    2628                         
     
    4143                            <td onclick='window.location = "${request.getContextPath()}/costCodeDetailed/show/${costCodeInstance.id}"'> 
    4244                                ${fieldValue(bean:costCodeInstance, field:'name')} 
     45                            </td> 
     46                         
     47                            <td onclick='window.location = "${request.getContextPath()}/costCodeDetailed/show/${costCodeInstance.id}"'> 
     48                                ${fieldValue(bean:costCodeInstance, field:'purchasingGroup')} 
    4349                            </td> 
    4450                         
  • trunk/grails-app/views/costCodeDetailed/show.gsp

    r441 r633  
    3434                     
    3535                        <tr class="prop"> 
     36                            <td valign="top" class="name">Purchasing Group:</td> 
     37                            <td valign="top" class="value"> 
     38                                <g:link controller="purchasingGroupDetailed" action="show" id="${costCodeInstance?.purchasingGroup?.id}"> 
     39                                    ${costCodeInstance?.purchasingGroup?.encodeAsHTML()} 
     40                                </g:link> 
     41                            </td> 
     42                        </tr> 
     43                     
     44                        <tr class="prop"> 
    3645                            <td valign="top" class="name">Description:</td> 
    3746                             
     
    4453                             
    4554                            <td valign="top" class="value">${fieldValue(bean:costCodeInstance, field:'isActive')}</td> 
    46                              
    47                         </tr> 
    48                      
    49                         <tr class="prop"> 
    50                             <td valign="top" class="name">Inventory Item Purchases:</td> 
    51                              
    52                             <td  valign="top" style="text-align:left;" class="value"> 
    53                                 <ul> 
    54                                 <g:each var="i" in="${costCodeInstance.inventoryItemPurchases}"> 
    55                                     <li><g:link controller="inventoryItemPurchaseDetailed" action="show" id="${i.id}">${i?.encodeAsHTML()}</g:link></li> 
    56                                 </g:each> 
    57                                 </ul> 
    58                             </td> 
    5955                             
    6056                        </tr> 
  • trunk/grails-app/views/inventoryItemPurchaseDetailed/create.gsp

    r609 r633  
    1515        <div class="body"> 
    1616            <g:render template="/shared/messages" /> 
     17            <g:if test="${!costCodes}" > 
     18                <div class="errors"> 
     19                    <ul> 
     20                        <li><g:message code="inventoryItemPurchase.costCodes.not.found" /><li> 
     21                </div> 
     22            </g:if> 
    1723            <g:hasErrors bean="${inventoryItemPurchaseInstance}"> 
    1824            <div class="errors"> 
     
    6672                                <td valign="top" class="value ${hasErrors(bean:inventoryItemPurchaseInstance,field:'costCode','errors')}"> 
    6773                                    <g:select optionKey="id" 
    68                                                         from="${ CostCode.findAllByIsActive(true).sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) } }" 
     74                                                        from="${ costCodes }" 
    6975                                                        name="costCode.id" 
    7076                                                        value="${inventoryItemPurchaseInstance?.costCode?.id}" 
  • trunk/grails-app/views/inventoryItemPurchaseDetailed/edit.gsp

    r609 r633  
    5959                                </td> 
    6060                            </tr>  
    61                          
     61 
    6262                            <tr class="prop"> 
    6363                                <td valign="top" class="name"> 
     
    6565                                </td> 
    6666                                <td valign="top" class="value ${hasErrors(bean:inventoryItemPurchaseInstance,field:'costCode','errors')}"> 
    67                                     <g:select optionKey="id" 
    68                                                         from="${ CostCode.findAllByIsActive(true).sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) } }" 
    69                                                         name="costCode.id" 
    70                                                         value="${inventoryItemPurchaseInstance?.costCode?.id}" > 
    71                                     </g:select> 
     67                                    <g:if test="${costCodes}"> 
     68                                        <g:select optionKey="id" 
     69                                                            from="${ costCodes }" 
     70                                                            name="costCode.id" 
     71                                                            value="${inventoryItemPurchaseInstance.costCode?.id}" > 
     72                                        </g:select> 
     73                                    </g:if> 
     74                                    <g:else> 
     75                                        <g:link controller="costCodeDetailed" action="show" id="${inventoryItemPurchaseInstance?.costCode?.id}"> 
     76                                            ${inventoryItemPurchaseInstance?.costCode?.encodeAsHTML()} 
     77                                        </g:link> 
     78                                    </g:else> 
    7279                                    <g:helpBalloon code="inventoryItemPurchase.cost.code" /> 
    7380                                </td> 
     
    96103                                                        from="${ Supplier.findAllByIsActive(true).sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) } }" 
    97104                                                        name="supplier.id" 
    98                                                         value="${inventoryItemPurchaseInstance?.supplier?.id}" 
    99                                                         noSelection="['null':/${g.message(code:'default.please.select.text')}/]"> 
     105                                                        value="${inventoryItemPurchaseInstance?.supplier?.id}"> 
    100106                                    </g:select> 
    101107                                    <g:helpBalloon code="inventoryItemPurchase.supplier" /> 
  • trunk/grails-app/views/person/create.gsp

    r506 r633  
    105105 
    106106                    <tr class="prop"> 
     107                        <td valign="top" class="name"> 
     108                            <label for="purchasingGroups">Purchasing Groups:</label> 
     109                        </td> 
     110                        <td valign="top" class="value ${hasErrors(bean:person,field:'purchasingGroups','errors')}"> 
     111                            <g:helpBalloon class="helpballoon" code="person.purchasingGroups" /> 
     112                            <custom:checkBoxList name="purchasingGroups" 
     113                                                            from="${PurchasingGroup.findAllByIsActive(true)}" 
     114                                                            value="${person?.purchasingGroups?.collect{it.id}}" 
     115                                                            optionKey="id" 
     116                                                            sortBy="name" 
     117                                                            linkController="purchasingGroupDetailed" 
     118                                                            linkAction="show"/> 
     119                            <g:link controller="purchasingGroupDetailed" action="create">+Add Group</g:link> 
     120                        </td> 
     121                    </tr> 
     122 
     123                    <tr class="prop"> 
    107124                        <td valign="top" class="name" align="left"> 
    108125                            Authorities: 
  • trunk/grails-app/views/person/edit.gsp

    r506 r633  
    144144 
    145145                    <tr class="prop"> 
     146                        <td valign="top" class="name"> 
     147                            <label for="purchasingGroups">Purchasing Groups:</label> 
     148                        </td> 
     149                        <td valign="top" class="value ${hasErrors(bean:person,field:'purchasingGroups','errors')}"> 
     150                            <g:helpBalloon class="helpballoon" code="person.purchasingGroups" /> 
     151                            <custom:checkBoxList name="purchasingGroups" 
     152                                                            from="${PurchasingGroup.findAllByIsActive(true)}" 
     153                                                            value="${person?.purchasingGroups?.collect{it.id}}" 
     154                                                            optionKey="id" 
     155                                                            sortBy="name" 
     156                                                            linkController="purchasingGroupDetailed" 
     157                                                            linkAction="show"/> 
     158                            <g:link controller="purchasingGroupDetailed" action="create">+Add Group</g:link> 
     159                        </td> 
     160                    </tr> 
     161 
     162                    <tr class="prop"> 
    146163                        <td valign="top" class="name" align="left"> 
    147164                            Authorities: 
  • trunk/grails-app/views/person/show.gsp

    r402 r633  
    8989                    <td valign="top" class="value"> 
    9090                        <ul> 
    91                         <g:each in="${person.personGroups}" var='group'> 
    92                             <li>${group}</li> 
     91                        <g:each var='group' in="${ person.personGroups.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) } }"> 
     92                            <li> 
     93                                <g:link controller="personGroupDetailed" 
     94                                                action="show" 
     95                                                id="${group.id}"> 
     96                                    ${group.encodeAsHTML()} 
     97                                </g:link> 
     98                            </li> 
     99                        </g:each> 
     100                        </ul> 
     101                    </td> 
     102                </tr> 
     103 
     104                <tr class="prop"> 
     105                    <td valign="top" class="name">Purchasing Groups:</td> 
     106                    <td valign="top" class="value"> 
     107                        <ul> 
     108                        <g:each  var='a' in="${ person.purchasingGroups.sort { p1, p2 -> p1.name.compareToIgnoreCase(p2.name) } }"> 
     109                            <li> 
     110                                <g:link controller="purchasingGroupDetailed" 
     111                                                action="show" 
     112                                                id="${a.id}"> 
     113                                    ${a.encodeAsHTML()} 
     114                                </g:link> 
     115                            </li> 
    93116                        </g:each> 
    94117                        </ul> 
Note: See TracChangeset for help on using the changeset viewer.