Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 58 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
58
Dung lượng
558,6 KB
Nội dung
CHAPTER 4 ■ UNDERSTANDING CONTROLLERS 95 The next step is to consider how to enable users to register, log in, and log out. Before you can do that, you need to define the notion of a user within the gTunes application. Let’s do that in the next section. Adding the User Domain Class To model users, you’ll need to create a User domain class that contains personal information such as first name and last name, as well as the login and password for each user. To do so, you can use the create-domain-class command: grails create-domain-class com.g2one.gtunes.User This will create a new domain class at the location grails-app/domain/com/g2one/gtunes/ User.groovy. With that done, you need to populate the User domain class with a few properties, as shown in Listing 4-47. Listing 4-47. The User Domain Class package com.g2one.gtunes class User { String login String password String firstName String lastName static hasMany = [purchasedSongs:Song] } As you can see, the code in Listing 4-47 captures only the basics about users, but you could easily expand this information to include an address, contact number, and so on. One property to note is the purchasedSongs association, which will hold references to all the Songs a User buys once you have implemented music purchasing. However, before we get too far ahead of ourselves, let’s add a few constraints to ensure domain instances stay in a valid state (see Listing 4-48). Listing 4-48. Applying Constraints to the User Class class User { static constraints = { login blank:false, size:5 15,matches:/[\S]+/, unique:true password blank:false, size:5 15,matches:/[\S]+/ firstName blank:false lastName blank:false } } With these constraints in place, you can ensure that a user cannot enter blank values or values that don’t fall within the necessary size constraints. Also, note the usage of the unique 96 CHAPTER 4 ■ UNDERSTANDING CONTROLLERS constraint, which ensures that the login property is unique to each User. We’ll revisit this in more detail later; for now, let’s focus on login and registration. Adding a Login Form Because you already have a home page, it might make sense to add the login form there. But fur- ther down the line, you’ll want to allow users to browse the gTunes music catalog anonymously, so users should be able to log in from anywhere. With this in mind, you need to add a login form to the grails-app/views/layouts/main.gsp layout so that it’s available on every page. Listing 4-49 shows the GSP code to do so. Note how you can check whether a User already exists in the session object and display a welcome box or login form, accordingly. Listing 4-49. Adding the Login Form Everywhere <div id="loginBox" class="loginBox"> <g:if test="${session?.user}"> <div style="margin-top:20px"> <div style="float:right;"> <a href="#">Profile</a> | <g:link controller="user" action="logout">Logout</g:link><br> </div> Welcome back <span id="userFirstName">${session?.user?.firstName}!</span> <br><br> You have purchased (${session.user.purchasedSongs?.size() ?: 0}) songs.<br> </div> </g:if> <g:else> <g:form name="loginForm" url="[controller:'user',action:'login']"> <div>Username:</div> <g:textField name="login" ></g:textField> <div>Password:</div> <g:passwordField name="password" /> <input type="submit" value="Login" /> </g:form> <g:renderErrors bean="${loginCmd}"></g:renderErrors> </g:else> </div> In addition to providing a login box, you need to provide a link that allows a User to regis- ter. Once logged in, the user will be able to click through the store to browse and click a “My Music” link to view music already purchased. These links won’t display when the user isn’t logged in, so instead you can use the screen real estate for a prominent link to the registration page. Listing 4-50 shows the registration link added to the main.gsp layout. CHAPTER 4 ■ UNDERSTANDING CONTROLLERS 97 Listing 4-50. Adding a Link to the Registration Page <div id="navPane"> <g:if test="${session.user}"> <ul> <li><g:link controller="user" action="music">My Music</g:link></li> <li><g:link controller="store" action="shop">The Store</g:link></a></li> </ul> </g:if> <g:else> <div id="registerPane"> Need an account? <g:link controller="user" action="register">Signup now</g:link> to start your own personal Music collection! </div> </g:else> </div> After getting the web designers involved and making a few Cascading Style Sheets (CSS) tweaks, the home page has gone from zero to something a little more respectable (see Figure 4-3). Figure 4-3. The gTunes home page Implementing Registration Before users can actually log in, they need to register with the site. You’ll need to run the create-controller command to create a controller that will handle the site’s login and regis- tration logic: grails create-controller com.g2one.gtunes.User Once complete, the command will create a controller at the location grails-app/ controllers/com/g2one/gtunes/UserController.groovy. Open up this controller and add a register action, as shown in Listing 4-51. 98 CHAPTER 4 ■ UNDERSTANDING CONTROLLERS Listing 4-51. Adding a register Action class UserController { def register = {} } As you can see from the example, the register action currently does nothing beyond delegating to a view. Nevertheless, it gives you the opportunity to craft a registration form. Listing 4-52 shows the shortened code from the grails-app/views/user/register.gsp view that will render the form. Listing 4-52. The register View <body id="body"> <h1>Registration</h1> <p>Complete the form below to create an account!</p> <g:hasErrors bean="${user}"> <div class="errors"> <g:renderErrors bean="${user}"></g:renderErrors> </div> </g:hasErrors> <g:form action="register" name="registerForm"> <div class="formField"> <label for="login">Login:</label> <g:textField name="login" value="${user?.login}" /> </div> <div class="formField"> <label for="password">Password:</label> <g:passwordField name="password" value="${user?.password}"/> </div> <g:submitButton class="formButton" name="register" value="Register" /> </g:form> </body> The rendered registration form will look like the screenshot in Figure 4-4. As you can see from Figure 4-4, you can also provide a confirm-password field to prevent users from entering their passwords incorrectly. With that done, let’s consider the controller logic. To implement registration, you can take advantage of Grails’ data-binding capabilities to bind incoming request parameters to a new User instance. At this point, validation takes over and the rest comes down to a little branching logic. Listing 4-53 shows the completed register action. CHAPTER 4 ■ UNDERSTANDING CONTROLLERS 99 Figure 4-4. The Registration screen Listing 4-53. Implementing the register Action 1 def register = { 2 if(request.method == 'POST') { 3 def u = new User(params) 4 if(u.password != params.confirm) { 5 u.errors.rejectValue("password", "user.password.dontmatch") 6 return [user:u] 7 } 8 else if(u.save()) { 9 session.user = u 10 redirect(controller:"store") 11 } 12 else { 13 return [user:u] 14 } 15 } 16 } Many of the key concepts you’ve learned throughout the course of this chapter have been put to use in Listing 4-53, including a few new ones. Let’s step through the code to see what’s going on. First, on line 2, the code checks that the incoming request is a POST request because doing all this processing is pointless unless a form is submitted: 2 if(request.method == 'POST') { Then on line 3, data binding takes over as it binds the incoming request parameters to the User instance: 3 def u = new User(params) 100 CHAPTER 4 ■ UNDERSTANDING CONTROLLERS On lines 4 though 7, the code confirms whether the user has entered the correct password twice. If not, the password is rejected altogether: 4 if(u.password != params.confirm) { 5 u.errors.rejectValue("password", "user.password.dontmatch") 6 return [user:u] 7 } Note how calling the rejectValue method of the org.springframework.validation. Errors interface accomplishes this. The rejectValue method accepts two arguments: the name of the field to reject and an error code to use. The code in Listing 4-53 uses the String user.password.dontmatch as the error code, which will appear when the <g:renderErrors> tag kicks in to display the errors. If you want to provide a better error message, you can open up the grails-app/i18n/messages.properties file and add a message like this: user.password.dontmatch=The passwords specified don't match Here’s one final thing to note: directly after the call to rejectValue, a model from the controller action is returned, which triggers the rendering register.gsp so it can display the error. Moving on to lines 8 through 11, you’ll notice that the code attempts to persist the User by calling the save() method. If the attempt is successful, the User is redirected back to the StoreController: 8 else if(u.save()) { 9 session.user = u 10 redirect(controller:"store") 11 } Finally, if a validation error does occur as a result of calling save(), then on line 13 a simple model is returned from the register action so that the register view can render the errors: 13 return [user:u] Testing the Registration Code Now let’s consider how to test the action using the ControllerUnitTestCase class you learned about earlier. When you ran the create-controller command, a new unit test for the UserController was created for you in the test/unit directory. You’ll notice that the UserControllerTests class extends from a super class called ControllerUnitTestCase: class UserControllerTests extends grails.test.ControllerUnitTestCase { Now write a test for the case in which a user enters passwords that don’t match. Listing 4-54 shows the testPasswordsDontMatch case that checks whether a password mis- match triggers a validation error. CHAPTER 4 ■ UNDERSTANDING CONTROLLERS 101 Listing 4-54. The testPasswordsMatch Test Case void testPasswordsMatch() { mockRequest.method = 'POST' mockDomain(User) mockParams.login = "joebloggs" mockParams.password = "password" mockParams.confirm = "different" mockParams.firstName = "Joe" mockParams.lastName = "Blogs" def model = controller.register() assert model?.user def user = model.user assert user.hasErrors() assertEquals "user.password.dontmatch", user.errors.password } Notice how the testPasswordsMatch test case populates the mockParams object with two passwords that differ. Then you have a call to the register action, which should reject the new User instance with a user.password.dontmatch error code. The last line of the test asserts that this is the case by inspecting the errors object on the User instance: assertEquals "user.password.dontmatch", user.errors.password The next scenario to consider is when a user enters invalid data into the registration form. You might need multiple tests that check for different kinds of data entered. Remember, you can never write too many tests! As an example of one potential scenario, Listing 4-55 shows a test that checks whether the user enters blank data or no data. Listing 4-55. The testRegistrationFailed Test void testRegistrationFailed() { mockRequest.method = 'POST' mockDomain(User) mockParams.login = "" def model = controller.register() 102 CHAPTER 4 ■ UNDERSTANDING CONTROLLERS assertNull mockSession.user assert model def user = model.user assert user.hasErrors() assertEquals "blank", user.errors.login assertEquals "nullable", user.errors.password assertEquals "nullable", user.errors.firstName assertEquals "nullable", user.errors.firstName } Once again, you can see the use of the errors object to inspect that the appropriate constraints have been violated. Finally, you need to ensure two things to test a successful registration: •The User instance has been placed in the session object. • The request has been redirected appropriately. Listing 4-56 shows an example of a test case that tests a successful user registration. Listing 4-56. Testing Successful Registration void testRegistrationSuccess() { mockRequest.method = 'POST' mockDomain(User) mockParams.login = "joebloggs" mockParams.password = "password" mockParams.confirm = "password" mockParams.firstName = "Joe" mockParams.lastName = "Blogs" def model = controller.register() assertEquals 'store',redirectArgs.controller assertNotNull mockSession.user } With the tests written, let’s now consider how to allow users to log in to the gTunes application. Allowing Users to Log In Since you’ve already added the login form, all you need to do is implement the controller logic. A login process is a good candidate for a command object because it involves capturing infor- mation—the login and password—without needing to actually persist the data. In this example you’re going to create a LoginCommand that encapsulates the login logic, leav- ing the controller action to do the simple stuff. Listing 4-57 shows the code for the LoginCommand class, which is defined in the same file as the UserController class. CHAPTER 4 ■ UNDERSTANDING CONTROLLERS 103 Listing 4-57. The LoginCommand class LoginCommand { String login String password private u User getUser() { if(!u && login) u = User.findByLogin(login, [fetch:[purchasedSongs:'join']]) return u } static constraints = { login blank:false, validator:{ val, cmd -> if(!cmd.user) return "user.not.found" } password blank:false, validator:{ val, cmd -> if(cmd.user && cmd.user.password != val) return "user.password.invalid" } } } The LoginCommand defines two properties that capture request parameters called login and password. The main logic of the code, however, is in the constraints definition. First, the blank constraint ensures that the login and/or password cannot be left blank. Second, a custom val- idator on the login parameter checks whether the user exists: login blank:false, validator:{ val, cmd -> if(!cmd.user) return "user.not.found" } The custom validator constraint takes a closure that receives two arguments: the value and the LoginCommand instance. The code within the closure calls the getUser() method of the LoginCommand to check if the User exists. If the User doesn’t exist, the code returns an error code—“user.not.found”—that signifies an error has occurred. On the password parameter, another custom validator constraint checks whether the User has specified the correct password: password blank:false, validator:{ val, cmd -> if(cmd.user && cmd.user.password != val) return "user.password.invalid" } Here the validator again uses the getUser() method of the LoginCommand to compare the password of the actual User instance with the value of the password property held by the LoginCommand. If the password is not correct, an error code is returned, triggering an error. You 104 CHAPTER 4 ■ UNDERSTANDING CONTROLLERS can add appropriate messages for each of the custom errors returned by the LoginCommand by adding them to the grails-app/i18n/messages.properties file: user.not.found=User not found user.password.invalid=Incorrect password With that done, it’s time to put the LoginCommand to use by implementing the login action in the UserController. Listing 4-58 shows the code for the login action. Listing 4-58. The login Action def login = { LoginCommand cmd -> if(request.method == 'POST') { if(!cmd.hasErrors()) { session.user = cmd.getUser() redirect(controller:'store') } else { render(view:'/store/index', model:[loginCmd:cmd]) } } else { render(view:'/store/index') } } With the command object in place, the controller simply needs to do is what it does best: issue redirects and render views. Again, like the register action, login processing kicks in only when a POST request is received. Then if the command object has no errors, the user is placed into the session and the request is redirected to the StoreController. Testing the Login Process Testing the login action differs slightly from testing the register action due to the involvement of the command object. Let’s look at a few scenarios that need to be tested. First, you need to test the case when a user is not found (see Listing 4-59). Listing 4-59. The testLoginUserNotFound Test Case void testLoginUserNotFound() { mockRequest.method = 'POST' mockDomain(User) MockUtils.prepareForConstraintsTests(LoginCommand) def cmd = new LoginCommand(login:"fred", password:"letmein") cmd.validate() controller.login(cmd) [...]... VIEWS Listing 5 -3 2 A Car Domain Class class Car { String make String model } Generate scaffolding for the Car class, and you will see that the default list action in the CarController and the default grails- app/view/car/list.gsp include support for paginating the list of cars Listing 5 -3 3 shows the relevant part of the GSP Listing 5 -3 3 grails- app/view/car/list.gsp ... class="paginateButtons"> The markup represented there renders an HTML table containing a header row and a row for each of the elements in the albums collection Notice the use of the paginate tag at the bottom of Listing 5 -3 0 That is all the code required in the GSP to render the pagination controls The paginate... based on the supplied attributes, which include the following: • controller: The controller name to link to • action: The action name to link to • id: The identifier to append to the end of the URI • params: Any parameters to pass as a map One of either the controller attribute or the action attribute is required If the controller attribute is specified but no action attribute is specified, the tag... container-specific One of the more useful of these is the contentType directive, which allows you to set the content type of the response This is useful in that it allows you to use GSP to output formats other than HTML markup, such as XML or plain text Using the directive is identical to JSP, with the directive appearing at the top of the page and starting with Here you get the total number of songs from the album and store them in the variable i You then start a loop that will decrement the i variable on each iteration The loop will continue until i reaches zero The loop is equivalent to the following Groovy code: while(i > 0) i=i-1 Using and are not the only way to loop over a collection In the next... reference to the params object via the ${ } expression syntax, which then allows passing parameters from the current page to the linked page Next you’ll see how to create links to other resources ■Note Grails linking tags automatically rewrite the links based on the URL mappings you have defined URL mappings will be covered in more detail in Chapter 6 The createLink and createLinkTo Tags The ... each view The application needs to provide support for requesting the batch of records that fall immediately before the current batch or immediately after the current batch The application needs to provide a mechanism for jumping straight to an area of the list, as opposed to navigating through the larger list a single page at a time The application needs to know the total number of records in the larger... expression like the following: ${album.title.toUpperCase()} CHAPTER 5 ■ UNDERSTANDING VIEWS Unfortunately, if either the album or title of the album in the previous code is null, a horrid NullPointerException will be thrown To circumvent this, the safe dereference operator comes to the rescue: ${album?.title?.toUpperCase()} Here the toUpperCase method is executed only if it can be reached; otherwise, the entire . "user.password.invalid" } Here the validator again uses the getUser() method of the LoginCommand to compare the password of the actual User instance with the value of the password property held by the LoginCommand. If the. is received. Then if the command object has no errors, the user is placed into the session and the request is redirected to the StoreController. Testing the Login Process Testing the login action. gives you the opportunity to craft a registration form. Listing 4-5 2 shows the shortened code from the grails- app/views/user/register.gsp view that will render the form. Listing 4-5 2. The register