Auto Binding a Grails One-To-Many Relationship - grails

I'm having difficulty auto-binding a one-to-many relationship in Grails without resorting to some hack in the controller. I understand that a one to many relationship in Grails is a set which is unordered and somehow affects binding.
When I save this form, sometimes the data saves correctly, and sometimes it does not. If an author has 3-4 books, it seems that it works less often.
In this example, I've tried to remove all non-relevant code to illustrate the issue.
Models:
class Author {
String name
static hasMany = [ books:Book ]
}
class Book {
String title
static belongsTo = [ author:Author ]
}
View:
<g:form method="post" class="form-horizontal">
<g:hiddenField name="id" value="${authorInstance?.id}" />
<g:hiddenField name="version" value="${authorInstance?.version}" />
<g:textField name='name' value='${authorInstance?.name}'/>
<g:each var="book" in="${authorInstance.books}" status="i">
<g:hiddenField name='book[${i}].id' value='${book.id}'/>
<g:textField name='book[${i}].title' value='${book.title}'/>
</g:each>
<g:actionSubmit action="update" value="Update" />
</g:form>
Controller:
def update(Long id, Long version) {
def author = Author.get(id)
// removed "Not Found" and "Version" validation for this example
author.properties = params
if (!author.save(flush: true)) {
render(view: "edit", model: [author: author])
return
}
flash.message = "Success"
redirect(action: "list"
}
How can I structure my model and view so I can leave the controller relatively untouched?

I've struggled with similar issues submitting one-to-many forms. I solved it in my app by converting the set to a bag.
So unless you specifically need books to be a set, try this:
class Author {
String name
Collection<Book> books
static hasMany = [ books:Book ]
}

I found that the easiest thing to do was force "Books" to be a List so it's ordered.
class Author {
List books <------- Added (by default this one-to-many relationship is a Set)
String name
static hasMany = [ books:Book ]
}
Then the view can remain the same and everything should work as expected.

Related

how to bind to sqldate?

I have a simple setup.
import java.sql.Date as SqlDate
class Book {
String name
SqlDate publishedDate
static constraints = {
}
}
In application.yml
grails:
databinding:
dateFormats:
- 'dd/MM/yyyy'
Book Controller save method
def save(){
def book = new Book()
bindData(book, params)
book.save()
render "done"
}
Create page
<g:form action="save">
<g:textField name="name"></g:textField>
<g:textField name="publishedDate"></g:textField>
<g:submitButton name="Submit"></g:submitButton>
</g:form>
When i use normal java date then the date binding works but when i use the sql.Date then bindData() doesnt seem to bind the date. Does bindData not work on sql.Date? Thanks for help!

Filtered results fields in grails

Hi I am new in Grails and would like to find out how do I have filtered results of two fields in grails. Should i do it in view, or should i do it in domain class? How do i do it?
In the following example, I have 3 classes:
Country
State
Address
The field State should display only filtered results based on Country. Right now all the states in the State object is presented. The following screenshot depicts this problem.
All the states are listed
The following are my codes.
class Country {
static constraints = {
}
String countryName
static hasMany = [state : State ]
}
class State {
static constraints = {
}
String stateName
static belongsTo = [ country : Country ]
}
class Address {
static constraints = {
}
String line1
String line2
Country country
State state
}
Any help is appreciated. Thank you so much.
Are you using scaffolding?
If so Grails is going to render all of the appropriate form controls unless you exclude them e.g.
static constraints = {
country display:false
}
However I don't think you need to associate Country with Address because there's an association between Address and State which also has an association with a Country so the association between an Address and a country will be implicit after selecting a state.
EDIT 1
If you want to keep the current model you'll probably have to move away from scaffolding and implement the action and gsp yourself e.g.
controller:
def create() {
def states
if ( params.country ) {
states = State.findByCountry(params.country)
}
[countries: Country.all, states: states]
}
gsp:
<g:select class="form-control"
name="country"
from="${countries}"
optionKey="${it}"
value="${params.country}"
required="required"
onchange="submit()"
noSelection="['':'Select country']" />
<g:if test="${params.country}">
<g:select class="form-control"
name="state"
from="${states}"
optionKey="${it}"
value="${params.state}"
required="required"
noSelection="['':'Select state']" />
</g:if>
The above should be treated as pseudo code to give an idea of possible solution

Formatting POJO inside Grails/GSP select element

I have the following POJO/POGO:
class Person {
String firstName
String lastName
int age
// ... lots of other fields
}
And a Grails 2.3.6 controller:
class PeopleController {
List<Person> people = new ArrayList<Person>()
def populatePeople() {
// Add lots of people to the 'people' list.
}
def doSomething() {
populatePeople()
render(
view: "people",
model:[
people: people,
]
)
}
}
And then in the GSP:
<div id="peopleSelector">
<g:select name="people" from="${people}" />
</div>
When I run my app I get the <select> element with com.me.myapp.domain.Person#398r4d99-looking values as <option>s. This is obviously Grails not deserializing my Person instances into pretty print form.
I want peoples' first and last names to appear as the select options. Hence, if one of the Person instances in the people list is:
Person smeeb = new Person(firstName: "Smeeb", lastNname: "McGuillocuty")
Then I would expect "Smeeb McGuillocuty" as a select option in the final HTML. How can I accomplish this?
Add the following method to your Person class:
#Override public String toString() {
"$firstName $lastName"
}
And, somewhat unrelated to the actual question, you may have to add an identifier to your option rows to uniquely identify the person. Assuming the Person class has an id property:
<g:select name="people" from="${people}" optionKey="id" />
so that you get the following HTML:
<select name="people" id="people">
<option value="123">Smeeb McGuillocuty</option>
:
Useful link to official doc: http://grails.org/doc/latest/ref/Tags/select.html:
"..The default behaviour is to call toString() on each element in the from attribute.."
If you can't/won't "sacrifice" toString() for rendering in HTML you can also tell the g:select how to render the options. Either by providing the name of a property in optionValue (e.g. optionValue="fullName" and then provide a String getFullName() method (watch out for transients, if you pass a GORM object)) or by providing it directly in the GSP:
<g:select name="person" optionKey="theId" optionValue='${{"$it.lastName, $it.firstName"}}' from="${people}" />

GORM: populate list into select dropdown

I am working on a Grails form item that just populates a list of FaqCategories from an FaqCategory domain class. All I want the select dropdown to do is display the list. So far, this sort of works, but I'm not sure how to generate the list or what to put in the Value section if anything.
Form item:
<div class="col-sm-9">
<g:select name="Category" from="${FaqCategory.list()}" required="" class="form-control" aria-labelledby="faqCategory-label" value=""/>
</div>
Domain class #1
class Faq {
String question
String answer
FaqCategory faqCategory
static constraints ={
question nullable:false, maxSize:1000
answer nullable:false, maxSize:1000
faqCategory nullable:false
}
}
Domain class #2
class FaqCategory {
String categoryType
String toString(){
"$categoryType"
}
static constraints = {
categoryType nullable:true, maxSize:100, unique:true
}
}
Controller snippet
#Transactional(readOnly = true)
def create() {
respond new Faq(params)
}
Controller code :
yourAction(int id){
//do some
def faqInstance = Faq.get(id)
render[view:"form",model:[faqInstance:faqInstance]]
}
View code :
<g:select name="faqCategory"
from="${FaqCategory.list()}"
value="${faqInstance?.faqCategory?.categoryType}"
optionKey="id" />
Hope it helps..
You haven't provided enough information to show the context in which this select is being used so it is hard to say exactly what you need, but something like this should work:
<g:select name="faqCategory"
from="${FaqCategory.list()}"
value="${you only need this if you want to preselect a value}"
optionKey="id" />
See http://grails.org/doc/latest/ref/Tags/select.html for more details.
I hope that helps.

Grails data binding one-to-many relationship

I am trying to take advantage of Grails' ability to handle data binding for a one-to-many association. I am able to successfully assigning to the relationship but removing single elements or all of them is not working.
Here is an example of what my model looks like:
class Location {
Float latitude
Float longitude
}
class Route {
Location start
Location stop
static belongsTo: [courier: Courier]
}
class Courier {
String name
static hasMany = [pickups: Location]
}
class ScheduledCourier extends Courier {
static hasMany = [routes: Route]
static mapping = {
routes(cascade: 'all-delete-orphan')
}
}
When creating a new ScheduledCourier object via a website, I can pass a list of Routes to automatically bind with markup like this:
<input type="hidden" name="routes[0].latitude" value="5" />
<input type="hidden" name="routes[0].longitude" value="5" />
<input type="hidden" name="routes[1].latitude" value="10" />
<input type="hidden" name="routes[1].longitude" value="10" />
This works for me just fine in my controller:
class CourierController {
// Very simplistic save
def saveScheduled = {
def courier = new ScheduledCourier(params)
courier.save()
}
// Very simplistic update
def update = {
def courier = Courier.get(params.id)
courier.properties = params
courier.save()
}
}
If I use the following markup instead, I can step through the debugger and see that the routes property is now [] and the object saves fine but the records are not removed from the database.
<input type="hidden" name="routes" value="" />
In addition, if I sent markup like this:
<input type="hidden" name="routes[0].latitude" value="5" />
<input type="hidden" name="routes[0].longitude" value="5" />
courier.routes will not be updated to only contain the 1 object.
Has anyone seen this behavior?
This is Grails 1.3.7...at least for now.
Wrote an integration test that reproduces this behavior:
public void testCourierSave() {
def l1 = new Location(latitude: 5, longitude: 5).save(flush: true)
def l2 = new Location(latitude: 10, longitude: 10).save(flush: true)
def params = ["name": "Courier", "pickups[0].id": l1.id, "pickups[1].id": l2.id,
"routes[0].start.id": l1.id, "routes[0].stop.id": l2.id,
"routes[1].start.id": l2.id, "routes[1].stop.id": l1.id]
def c1 = new ScheduledCourier(params).save(flush: true)
assertEquals(2, c1.routes.size())
params = [routes: ""]
c1.properties = params
c1.save(flush: true)
c1.refresh() // Since the Routes aren't deleted, this reloads them
assertEquals(0, c1.routes.size()) // Fails
assertEquals([], Route.findAllByCourier(c1)) // Fails if previous assert is removed
}
I wonder if the following is happening:
When passing the params [routes:""] the framework is ignoring it as it's just an empty string.
Similarly <input type="hidden" name="routes[0].latitude" value="5" /> probably just updates the zeroth route entry in the collection, the others aren't deleted because all you've told it is that the latitude value of the zeroth route should be 5, not that this is should now be the only route in the collection.
To get the effect you want, you'll need to add a routes.clear() before binding the parameters.
To control when the state of the model is persisted to the database you can use Spring transactionality which is available in Grails. This would allow you to revert to the original state of the object if subsequent processing failed. eg:
Courier.withTransaction {status ->
// Load the courier
//clear the existing routes
// bind the new properties
// perform extra processing
//if the object is invalid, roll back the changes
status.setRollbackOnly()
}

Resources