0

Consider the following class:

Class Timesheet {
    BigDecimal hoursWorked
    Boolean reviewedByCustomer
    Boolean approvedByCustomer
    ...
}

The timesheet can have three states in terms of customer review:

  1. TO_BE_CHECKED (reviewedByCustomer == false && approvedByCustomer == null)
  2. APPROVED (reviewedByCustomer == true && approvedByCustomer == true)
  3. DENIED (reviewedByCustomer == false && approvedByCustomer == false)

I want to use an enum type ReviewStatus to represent these states that can be retrieved from a timesheet or used to update the timesheet. The two boolean values shall not be used anymore. With the following parameter map: [reviewStatus:'APPROVED'], data binding should work as follows .

def timesheet = new Timesheet(params)

or

bindData(timesheet, params)

The Status should be checked as follows:

if(timesheet.reviewStatus == ReviewStatus.TO_BE_REVIEWED){
    //do Logic
}

To achieve this behaviour, I use a transient property and getter and setter methods:

...

//reviewStatus does only exist as getter and setter methods, not as fields
static transients = ['reviewStatus']

ReviewStatus getReviewStatus(){
    if(reviewedByCustomer == false && approvedByCustomer == null){
        ReviewStatus.TO_BE_REVIEWED
    } else if(reviewedByCustomer == true && approvedByCustomer == true){
        ReviewStatus.APPROVED 
    } else if(reviewedByCustomer == true && approvedByCustomer == false){
        ReviewStatus.DENIED
    }
}

void setReviewStatus(ReviewStatus reviewStatus){
    if(reviewStatus == ReviewStatus.TO_BE_REVIEWED){
        reviewedByCustomer = false
        approvedByCustomer = null
    } else if(reviewStatus == ReviewStatus.APPROVED){
        reviewedByCustomer = true
        approvedByCustomer = true
    } else if(reviewStatus == ReviewStatus.DENIED){
        reviewedByCustomer = true
        approvedByCustomer = false
    }
}
...

However, it does not work. Not even with bindable:true. I found this as an answer for similar questions, but they seem to have been using an earlier version of Grails. The only way I could get it to work was by using bindData(object, params, [exclude:[]]). I assume that the empty map prevents the transient properties from being added to the exclusion list automatically.

I would prefer to use the bindable constraint instead, because this would be a cleaner solution than passing an empty map every time I bind data to a timesheet

Using Grails 2.4.2.

EDIT 1: Using Grails 2.4.2 data binder, not spring data binder.

nst1nctz
  • 333
  • 3
  • 23
  • 1
    There is a lot of noise in the question. I can't really tell what it is that you want. I wrote the data binder and I am happy to help with data binding recommendations but I don't understand what it is that you want to do based on this long question. – Jeff Scott Brown Nov 12 '15 at 19:36
  • How did you code the getter/setter methods? – Emmanuel Rosa Nov 13 '15 at 00:28
  • @JeffScottBrown Sorry about the incomplete question, but I was in a hurry. I rephrased the question and hope that it is more readable now. – nst1nctz Nov 13 '15 at 07:26
  • @EmmanuelRosa I added the source code. – nst1nctz Nov 13 '15 at 07:27
  • Is the whole question just that you want to know how to do data binding to an enum property that is not backed by a field? – Jeff Scott Brown Nov 13 '15 at 12:32
  • The question asserts "However, it does not work.". It isn't clear which part isn't working and how it not working manifests. Can you clarify? The example I posted below uses the code you show and it does appear to work. At least the data binding works, which appears to be what you are asking about. – Jeff Scott Brown Nov 13 '15 at 14:18
  • @JeffScottBrown Yes, basically it is about using an enum property in the application logic and two boolean properties in domain class and database. I'm also wondering if that is the "proper" way to use transients (i.e. without a real field, just getter and setter). Somehow the grails data binder seems not to be wanting me binding data to transients, so i figured transients may be the wrong approach. – nst1nctz Nov 13 '15 at 18:08
  • Issue not reproducable. Above code works with the specified environment. – nst1nctz Nov 23 '15 at 16:59

1 Answers1

0

The project at https://github.com/jeffbrown/enumprop demonstrates one way to do this.

Domain Class:

// grails-app/domain/demo/Timesheet.groovy
package demo

class Timesheet {
    Boolean reviewedByCustomer
    Boolean approvedByCustomer

    static transients = ['reviewStatus']

    ReviewStatus getReviewStatus(){
        if(reviewedByCustomer == false && approvedByCustomer == null){
            ReviewStatus.TO_BE_REVIEWED
        } else if(reviewedByCustomer == true && approvedByCustomer == true){
            ReviewStatus.APPROVED
        } else if(reviewedByCustomer == true && approvedByCustomer == false){
            ReviewStatus.DENIED
        }
    }

    void setReviewStatus(ReviewStatus reviewStatus){
        if(reviewStatus == ReviewStatus.TO_BE_REVIEWED){
            reviewedByCustomer = false
            approvedByCustomer = null
        } else if(reviewStatus == ReviewStatus.APPROVED){
            reviewedByCustomer = true
            approvedByCustomer = true
        } else if(reviewStatus == ReviewStatus.DENIED){
            reviewedByCustomer = true
            approvedByCustomer = false
        }
    }
}

Unit test:

// test/unit/demo/TimesheetSpec.groovy
package demo

import grails.test.mixin.TestFor
import spock.lang.Specification
import spock.lang.Unroll

@TestFor(Timesheet)
class TimesheetSpec extends Specification {

    @Unroll('When reviewStatus is #reviewStatus reviewedByCustomer should be #reviewedByCustomer and approvedByCustomer should be #approvedByCustomer')
    void "test enum property binding"() {
        given:
        def timesheet = new Timesheet(reviewStatus: reviewStatus)

        expect:
        timesheet.reviewedByCustomer == reviewedByCustomer
        timesheet.approvedByCustomer == approvedByCustomer

        where:
        reviewStatus     | reviewedByCustomer | approvedByCustomer
        'APPROVED'       | true               | true
        'DENIED'         | true               | false
        'TO_BE_REVIEWED' | false              | null
    }

    @Unroll('When reviewedByCustomer is #reviewedByCustomer and approvedByCustomer is #approvedByCustomer then reviewStatus should be #reviewStatus')
    void "test retrieving the value of the enum property"() {
        given:
        def timesheet = new Timesheet(reviewedByCustomer: reviewedByCustomer,
                                      approvedByCustomer: approvedByCustomer)

        expect:
        timesheet.reviewStatus == reviewStatus

        where:
        reviewStatus                | reviewedByCustomer | approvedByCustomer
        ReviewStatus.APPROVED       | true               | true
        ReviewStatus.DENIED         | true               | false
        ReviewStatus.TO_BE_REVIEWED | false              | null
    }
}
Jeff Scott Brown
  • 26,804
  • 2
  • 30
  • 47
  • The suggestion above assumes that you are using the default data binder in 2.4.2 and not the deprecated Spring data binder. – Jeff Scott Brown Nov 13 '15 at 13:10
  • Thanks a lot for taking your time testing this. This is exactly how I want it to work, just also with `new Timesheet(params)`. Have you tried that as well? I have not tried using the map constructor just yet. Also, I am using the 2.4.2. data binder. – nst1nctz Nov 13 '15 at 18:10
  • "This is exactly how I want it to work, just also with new Timesheet(params)" - That is what this test is doing. `params` is a `Map`. The test above is invoking the same constructor that `new Timesheet(params)` would invoke in a controller. – Jeff Scott Brown Nov 13 '15 at 19:07
  • Yes, I just realized that. I am currently trying to write a unit test that reproduces my issue. – nst1nctz Nov 13 '15 at 19:14
  • Maybe use https://github.com/jeffbrown/enumprop/commit/04f0c9018b92dbdd63d257b34bc194ed13bb8467 as a starting point. – Jeff Scott Brown Nov 13 '15 at 19:25
  • Does `reviewStatus bindable: true` in your `constraints` block help? – Jeff Scott Brown Nov 13 '15 at 19:26
  • In looking at this I see a bug in the binder but the bug is the reason that the code I wrote above works. It isn't clear what is different in your app. Are you sure you are using 2.4.2? – Jeff Scott Brown Nov 13 '15 at 19:42
  • That is partly why I am asking this question: is this a good way to solve this or is it merely a hack, using a bug in the binder? I was unable to reproduce it in a unit test yet, but I am definitely using 2.4.2. I am experimenting with abstract classes and inheritance, since the real application is quite complex. In this simple example everything seems to work just fine though... – nst1nctz Nov 13 '15 at 20:00
  • No, bindable does not help. The only thing that worked for me in my application was using `bindData(object, params, [excluded:[]])` in my controller, which I do not want to use. And if this only works because of a bug, I won't use it either. Now I am reconsidering just adapting the database to the changes in the domain model. I feel like it is usually easier to stick to the grails conventions :) – nst1nctz Nov 13 '15 at 20:10
  • By the way, the new grails data binding is awesome! I used grails 1.3.7 for quite some time, and after upgrading to 2.4.2 I was stunned how everything works now. It's just edge cases like that where I struggle. – nst1nctz Nov 13 '15 at 20:23
  • "I am experimenting with abstract classes and inheritance, since the real application is quite complex." - If you can identify how to recreate what you are seeing, that would be helpful. – Jeff Scott Brown Nov 14 '15 at 13:57
  • I am not able to reproduce the issue anymore, my initial code works now. I must have done something wrong at another place, which is probably fixed now. If I am able to identify the real problem at some point, I will let you know. Apreciate your time to help nonetheless. Next time I will unit test more thoroughly before asking :) – nst1nctz Nov 23 '15 at 16:58