|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Creating a Rails admin panel from scratch, part 3: Other resources" |
| 4 | +excerpt: "In the final part of this series, we'll create a fresh controller and views to manage an ActiveAdmin resource within our namespaced admin panel." |
| 5 | +--- |
| 6 | + |
| 7 | +Over the past couple of weeks I've been sharing a technique for building namespaced administration interfaces into Rails applications. So far in this series we've [created a basic administration dashboard](http://everydayrails.com/2012/07/31/rails-admin-panel-from-scratch.html) and [leveraged Rails scaffolds](http://everydayrails.com/2012/08/07/rails-admin-panel-from-scratch-2.html) to add some functionality to it. In this final post we'll add another round of functionality--this time, for a model that already exists in an application. |
| 8 | + |
| 9 | +The incredibly simple blogging application includes a `User` model to allow authors to log in and create posts. However, so far the application doesn't yet have any functionality to manage those users. We'll add that now, once again following a test-driven approach with RSpec. (As a reminder-slash-shameless plug, if you're interested in a simple way to get going with RSpec and TDD, [check out my book](https://leanpub.com/everydayrailsrspec) on the subject to get started). The [application's source](https://github.com/ruralocity/admin_demo) can be seen in its entirety on GitHub; see the the [resources branch](https://github.com/ruralocity/admin_demo/tree/resource) for code specific to this post. |
| 10 | + |
| 11 | +## Adding a user |
| 12 | + |
| 13 | +Let's get going: We'll start with an interface for adding new users. Here's the request spec we'll make pass: |
| 14 | + |
| 15 | +{% highlight ruby %} |
| 16 | + # spec/requests/admin_spec.rb |
| 17 | + |
| 18 | + describe 'user management' do |
| 19 | + before :each do |
| 20 | + user = FactoryGirl.create(:user) |
| 21 | + sign_in user |
| 22 | + end |
| 23 | + |
| 24 | + it "adds a user" do |
| 25 | + click_link 'Manage Users' |
| 26 | + current_path.should eq admin_users_path |
| 27 | + |
| 28 | + expect{ |
| 29 | + click_link 'New User' |
| 30 | + fill_in 'Email', with: ' [email protected]' |
| 31 | + fill_in 'Password', with: 'secret' |
| 32 | + fill_in 'Password confirmation', with: 'secret' |
| 33 | + click_button 'Create User' |
| 34 | + }.to change(User, :count).by(1) |
| 35 | + |
| 36 | + current_path.should eq admin_users_path |
| 37 | + page.should have_content ' [email protected]' |
| 38 | + end |
| 39 | + end |
| 40 | +{% endhighlight %} |
| 41 | + |
| 42 | +If you read the previous post in this series, this should look pretty familiar; we'll create a similar interface for user management now. Following test-driven development, our spec fails pretty quickly: |
| 43 | + |
| 44 | + 1) site administration user management adds a user |
| 45 | + Failure/Error: current_path.should eq admin_users_path |
| 46 | + NameError: |
| 47 | + undefined local variable or method `admin_users_path' for #<RSpec::Core::ExampleGroup::Nested_4::Nested_3:0x007f83c940d658> |
| 48 | + |
| 49 | +Again, these failures should look pretty familiar by now. In this case we need to add a namespaced route for managing users: |
| 50 | + |
| 51 | +{% highlight ruby %} |
| 52 | + # config/routes.rb |
| 53 | + |
| 54 | + namespace :admin do |
| 55 | + get '', to: 'dashboard#index', as: '/' |
| 56 | + resources :articles |
| 57 | + resources :users |
| 58 | + end |
| 59 | +{% endhighlight %} |
| 60 | + |
| 61 | +The next failure points to the fact that the dashboard we created back in the first post in this series is missing a complete link for managing users. |
| 62 | + |
| 63 | + 1) site administration user management adds a user |
| 64 | + Failure/Error: current_path.should eq admin_users_path |
| 65 | + |
| 66 | + expected: "/admin/users" |
| 67 | + got: "/admin" |
| 68 | + |
| 69 | + (compared using ==) |
| 70 | + |
| 71 | +Easy enough to fix: |
| 72 | + |
| 73 | +{% highlight erb %} |
| 74 | + <!-- app/views/admin/dashboard/index.html.erb --> |
| 75 | + |
| 76 | + <h1>Administration</h1> |
| 77 | + |
| 78 | + <ul> |
| 79 | + <li><%= link_to 'Manage Users', admin_users_path %></li> |
| 80 | + <li><%= link_to 'Manage Articles', admin_articles_path %></li> |
| 81 | + </ul> |
| 82 | +{% endhighlight %} |
| 83 | + |
| 84 | +Another step forward, and another failed expectation: |
| 85 | + |
| 86 | + 1) site administration user management adds a user |
| 87 | + Failure/Error: click_link 'Manage Users' |
| 88 | + ActionController::RoutingError: |
| 89 | + uninitialized constant Admin::UsersController |
| 90 | + |
| 91 | +Looks like we need to add a controller. Unlike the previous post, where we moved existing files to add admin functionality, this time we'll add it from scratch: |
| 92 | + |
| 93 | +{% highlight bash %} |
| 94 | + rails generate controller admin/users index |
| 95 | +{% endhighlight %} |
| 96 | + |
| 97 | +This creates the controller in `app/controllers/admin`, as well as an ERB template we'll get to next in `app/views/admin/users`. (It also adds the route ` get "users/index"` to `config/routes.rb`; we're not going to use that route so we can delete it.) In fact, the next failure points to something missing in this template: |
| 98 | + |
| 99 | + 1) site administration user management adds a user |
| 100 | + Failure/Error: click_link 'New User' |
| 101 | + Capybara::ElementNotFound: |
| 102 | + no link with title, id or text 'New User' found |
| 103 | + |
| 104 | +The view template will eventually include the ability to list existing users, of course, but for now let's just do the minimum amount of work to make this expectation pass: |
| 105 | + |
| 106 | +{% highlight erb %} |
| 107 | + <!-- app/views/admin/users/index.html.erb --> |
| 108 | + |
| 109 | + <h1>Listing users</h1> |
| 110 | + |
| 111 | + <%= link_to 'New User', new_admin_user_path %> |
| 112 | +{% endhighlight %} |
| 113 | + |
| 114 | +And another failure: |
| 115 | + |
| 116 | + Failures: |
| 117 | + |
| 118 | + 1) site administration user management adds a user |
| 119 | + Failure/Error: click_link 'New User' |
| 120 | + AbstractController::ActionNotFound: |
| 121 | + The action 'new' could not be found for Admin::UsersController |
| 122 | + |
| 123 | +No problem--the controller we created a moment ago doesn't have a `new` method, but we can easily add that now. |
| 124 | + |
| 125 | +{% highlight ruby %} |
| 126 | + # app/controllers/admin/users_controller.rb |
| 127 | + |
| 128 | + class Admin::UsersController < ApplicationController |
| 129 | + def index |
| 130 | + end |
| 131 | + |
| 132 | + def new |
| 133 | + end |
| 134 | + end |
| 135 | +{% endhighlight %} |
| 136 | + |
| 137 | +The next failure suggests the method needs a corresponding view: |
| 138 | + |
| 139 | + 1) site administration user management adds a user |
| 140 | + Failure/Error: click_link 'New User' |
| 141 | + ActionView::MissingTemplate: |
| 142 | + Missing template admin/users/new, application/new with {:locale=>[:en], :formats=>[:html], :handlers=>[:erb, :builder, :coffee]}. Searched in: |
| 143 | + * "/Users/asumner/Sites/Rails/admin_demo/app/views" |
| 144 | + |
| 145 | +So let's go ahead and add that--just a blank file at `app/views/admin/users/new.html.erb` is all we need to push the process forward. Now RSpec will complain about not finding the actual form we've told it to expect: |
| 146 | + |
| 147 | + 1) site administration user management adds a user |
| 148 | + Failure/Error: fill_in 'Email', with: '[email protected]' |
| 149 | + Capybara::ElementNotFound: |
| 150 | + cannot fill in, no text field, text area or password field with id, name, or label 'Email' found |
| 151 | + |
| 152 | +I'm just going to use the general style of form as Rails' scaffold generators would provide to place the form in the `new.html.erb` template we created a second ago: |
| 153 | + |
| 154 | +{% highlight erb %} |
| 155 | + <!-- app/views/admin/users/new.html.erb --> |
| 156 | + |
| 157 | + <h1>New user</h1> |
| 158 | + |
| 159 | + <%= form_for([:admin,@user]) do |f| %> |
| 160 | + <% if @user.errors.any? %> |
| 161 | + <div id="error_explanation"> |
| 162 | + <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2> |
| 163 | + |
| 164 | + <ul> |
| 165 | + <% @user.errors.full_messages.each do |msg| %> |
| 166 | + <li><%= msg %></li> |
| 167 | + <% end %> |
| 168 | + </ul> |
| 169 | + </div> |
| 170 | + <% end %> |
| 171 | + |
| 172 | + <div class="field"> |
| 173 | + <%= f.label :email %><br /> |
| 174 | + <%= f.text_field :email %> |
| 175 | + </div> |
| 176 | + <div class="field"> |
| 177 | + <%= f.label :password %><br /> |
| 178 | + <%= f.password_field :password %> |
| 179 | + </div> |
| 180 | + <div class="field"> |
| 181 | + <%= f.label :password_confirmation %><br /> |
| 182 | + <%= f.password_field :password_confirmation %> |
| 183 | + </div> |
| 184 | + <div class="actions"> |
| 185 | + <%= f.submit %> |
| 186 | + </div> |
| 187 | + <% end %> |
| 188 | +{% endhighlight %} |
| 189 | + |
| 190 | +Which yields a fresh RSpec failure: |
| 191 | + |
| 192 | + 1) site administration user management adds a user |
| 193 | + Failure/Error: click_link 'New User' |
| 194 | + ActionView::Template::Error: |
| 195 | + undefined method `model_name' for NilClass:Class |
| 196 | + |
| 197 | +We just need something to pass to `@user` in the form--this is easily remedied in the controller: |
| 198 | + |
| 199 | +{% highlight ruby %} |
| 200 | + # app/controllers/admin/users_controller.rb |
| 201 | + |
| 202 | + class Admin::UsersController < ApplicationController |
| 203 | + def index |
| 204 | + end |
| 205 | + |
| 206 | + def new |
| 207 | + @user = User.new |
| 208 | + end |
| 209 | + end |
| 210 | +{% endhighlight %} |
| 211 | + |
| 212 | +Now the spec is looking for a `create` method in the Users controller. |
| 213 | + |
| 214 | + 1) site administration user management adds a user |
| 215 | + Failure/Error: click_button 'Create User' |
| 216 | + AbstractController::ActionNotFound: |
| 217 | + The action 'create' could not be found for Admin::UsersController |
| 218 | + |
| 219 | +Again, I'm just going to follow the style this method would have had it been generated by a Rails scaffold. |
| 220 | + |
| 221 | +{% highlight ruby %} |
| 222 | + # app/controllers/admin/users_controller.rb |
| 223 | + |
| 224 | + def create |
| 225 | + @user = User.new(params[:user]) |
| 226 | + |
| 227 | + respond_to do |format| |
| 228 | + if @user.save |
| 229 | + format.html { redirect_to admin_users_url, notice: 'User was successfully created.' } |
| 230 | + format.json { render json: @user, status: :created, location: [:admin,@user] } |
| 231 | + else |
| 232 | + format.html { render action: "new" } |
| 233 | + format.json { render json: @user.errors, status: :unprocessable_entity } |
| 234 | + end |
| 235 | + end |
| 236 | + end |
| 237 | +{% endhighlight %} |
| 238 | + |
| 239 | +We're getting really close now, but we've got a couple more things to take care of. First, a new failed expectation: |
| 240 | + |
| 241 | + 1) site administration user management adds a user |
| 242 | + Failure/Error: page.should have_content '[email protected]' |
| 243 | + expected there to be content "[email protected]" in "AdminDemo\n\n Logged in as [email protected].\n Log Out\n\n Listing users\n\nNew User\n\n\n" |
| 244 | + |
| 245 | +Let's go back to the `index` template to add this. |
| 246 | + |
| 247 | +{% highlight erb %} |
| 248 | + <!-- app/views/admin/users/index.html.erb --> |
| 249 | + |
| 250 | + <h1>Listing users</h1> |
| 251 | + |
| 252 | + <table> |
| 253 | + <tr> |
| 254 | + <th>Email</th> |
| 255 | + </tr> |
| 256 | + |
| 257 | + <% @users.each do |user| %> |
| 258 | + <tr> |
| 259 | + <td><%= user.email %></td> |
| 260 | + </tr> |
| 261 | + <% end %> |
| 262 | + </table> |
| 263 | + |
| 264 | + <br /> |
| 265 | + |
| 266 | + <%= link_to 'New User', new_admin_user_path %> |
| 267 | +{% endhighlight %} |
| 268 | + |
| 269 | +Oops, a fresh failure: |
| 270 | + |
| 271 | + 1) site administration user management adds a user |
| 272 | + Failure/Error: click_link 'Manage Users' |
| 273 | + ActionView::Template::Error: |
| 274 | + undefined method `each' for nil:NilClass |
| 275 | + |
| 276 | +Remedied by a quick addition to the controller: |
| 277 | + |
| 278 | +{% highlight ruby %} |
| 279 | + # app/controllers/admin/users_controller.rb |
| 280 | + |
| 281 | + def index |
| 282 | + @users = User.all |
| 283 | + end |
| 284 | +{% endhighlight %} |
| 285 | + |
| 286 | +And now the request spec should pass! Blog editors--anyone with an account, basically--can now add more users through the admin panel. As in the previous post, however, we've still got a little more to do. This spec tests the happy path: A known user logs in and enters a user correctly in the form. We've got some other cases to test and potential functionality to add: |
| 287 | + |
| 288 | +1. What happens if someone accesses the form without logging in first? |
| 289 | +2. What happens if the password and password confirmation don't match? |
| 290 | +3. What happens if an incorrectly-formatted email address is entered? |
| 291 | +4. What happens if a duplicate email address is entered? |
| 292 | + |
| 293 | +I'm not going to explicitly go through these here, but you can look at the source on GitHub for more details. Short answers: |
| 294 | + |
| 295 | +1. Test this at the controller and make sure to `authorize` the new controller. |
| 296 | +2. The way we've implemented passwords uses the `has_secure_password` functionality built into Rails; it should therefore be adequately tested already without us adding another. |
| 297 | +3. Add a custom validation to the `User` model and test at that level. You could get pretty detailed with this if you wish--I included a couple of examples in the full source. |
| 298 | +4. This would also be tested at the model level (though since uniqueness validations are built into Rails, one might argue that it's not strictly necessary to test). In the interest of completion, it wouldn't hurt to add a controller-level test to make sure the controller (in this case, the `create` method) properly handles invalid form submissions, too. |
| 299 | + |
| 300 | +## Next steps |
| 301 | + |
| 302 | +That's about it for this exercise. Of course, you may need more functionality for your user management, such as editing existing accounts or suspending them from login. I'll leave that up to you to implement in your own applications. |
| 303 | + |
| 304 | +The interface is also pretty spartan--you might want to spruce it up with something like Twitter Bootstrap, or add filtering functionality with [Ransack](http://rubygems.org/gems/ransack). Again, I'll leave that up to you--the point of this series has been to start with the basics and build up from there. |
| 305 | + |
| 306 | +## Wrapping up |
| 307 | + |
| 308 | +That will wrap up this series on creating administration interfaces for your Rails applications, without a soup-to-nuts solution like ActiveAdmin. We not only built an admin panel--we also looked at how to namespace Rails routes (pretty easily, as it turns out) and how to use RSpec request specs to drive the development. I hope you found this series useful. |
0 commit comments