Tutorial Details
- Difficulty: Medium
- Framework: RoR
- Estimated Completion Time: 1 hour
Every web developer knows that creating an administration interface for their projects is an incredibly tedious task. Luckily, there are tools that make this task considerably simpler. In this tutorial, I’ll show you how to use Active Admin, a recently launched administration framework for Ruby on Rails applications.
You can use Active Admin to add an administration interface to your current project, or you can even use it to create a complete web application from scratch – quickly and easily.
Today, we’ll be doing the latter, by creating a fairly simple project management system. It might sound like quite a bit of work, but Active Admin will do the bulk of the work for us!
Step 1 – Set up the Development Environment
I’m going to assume you have some previous Ruby on Rails knowledge, especially involving model validations, since the rest of the application interface is going to be taken care of by Active Admin. Apart from that, you should have a development environment for Ruby on Rails 3.1 already set up, including Ruby 1.9.2.
Refer to this article if you require assistance installing Ruby and Rails.
Create the application we’ll be working on, by running the following command in your Terminal:
Next, open your Gemfile and add the following lines:
|
1
2
|
gem 'activeadmin'
gem 'meta_search', '>= 1.1.0.pre'
|
The last gem is required for Active Admin to work with Rails 3.1, so don’t forget it. After that’s done, run the bundle install command to install the gems. Now, we need to finish installing Active Admin, by running the following command:
|
1
|
rails generate active_admin:install
|
This will generate all needed initializers and migrations for Active Admin to work. It will also create an AdminUser model for authentication, so run rake db:migrate to create all the needed database tables. Apart from that, you need to add one line to your config/environments/development.rb file, so sending emails works:
|
1
|
config.action_mailer.default_url_options = { :host => 'localhost:3000' }
|
Once that’s done, run rails server and point your browser to localhost:3000/admin. You’ll be greeted with a nice login form. Just type “[email protected]” as the email and “password” as the password, and hit “Login”. You should now see a nice administration interface.
Step 2 – Configuring our User Model
As you can see from the webpage you just generated, there’s not much you can do, yet. We’re going to need a way to edit our users, and we can do that using Active Admin. The framework uses what it calls “Resources”. Resources map models to administration panels. You need to generate them using a command in your terminal, so Active Admin can know their existence, so go ahead and run:
|
1
|
rails generate active_admin:resource AdminUser
|
The syntax for that command is simple: just write the database model’s name at the end. This will generate a file inside the app/admin folder, called admin_users.rb. Now, if you refresh your browser you’ll see a new link at the top bar, called “Admin Users”. Clicking that will take you to the Admin User administration panel. Now, it’ll probably look a little too cluttered, since by default, Active Admin shows all of the model’s columns, and considering that the framework uses Devise for authentication, you’ll see a bunch of columns that are not really necessary. This takes us to the first part of our customization: the index page.
Customizing Active Admin resources is fairly easy (and fun if you ask me). Open app/admin/admin_users.rb on your favorite text editor and make it look like this:
|
1
2
3
4
5
6
7
8
9
|
ActiveAdmin.register AdminUser do
index do
column :email
column :current_sign_in_at
column :last_sign_in_at
column :sign_in_count
default_actions
end
end
|
Let’s review the code:
- The first line is created by Active Admin, and, like it says, it registers a new resource. This created the menu link at the top bar and all of the default actions, like the table you just saw.
- The
index method allows us to customize the index view, which is the table that shows all rows.
- Inside of the block you pass to the
index method, you specify which columns you do want to appear on the table, ie. writing column :email will have Active Admin show that column on the view.
default_actions is a convenience method that creates one last column with links to the detail, edition and deletion of the row.
One final step for this view is to customize the form. If you click the “New Admin User” link on the top right, you’ll see that the form also contains all of the columns on the model, which is obviously not very useful. Since Active Admin uses Devise, we only need to enter an email address to create a user, and the rest should be taken care of by the authentication gem. To customize the forms that Active Admin displays, there’s a method, called (you guessed it) form:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
ActiveAdmin.register AdminUser do
index do
# ...
end
form do |f|
f.inputs "Admin Details" do
f.input :email
end
f.buttons
end
end
|
If the code looks familiar to you, you’ve probably used the Formtastic gem before. Let’s take a look at the code:
- You specify the form’s view by calling the
form method and passing it a block with an argument (f in this case).
f.inputs creates a fieldset. Word of advice: you have to add at least one fieldset. Fields outside of one will simply not appear on the view.
- To create a field, you simply call
f.input and pass a symbol with the name of the model’s column, in this case, “email”.
f.buttons creates the “Submit” and “Cancel” buttons.
You can further customize the forms using the DSL (Domain Specific Language) provided by Formtastic, so take a look at tutorials about this gem.
One last step for this form to work: since we’re not providing a password, Devise is not going to let us create the record, so we need to add some code to the AdminUser model:
|
1
2
3
4
5
|
after_create { |admin| admin.send_reset_password_instructions }
def password_required?
new_record? ? false : super
end
|
The after_create callback makes sure Devise sends the user a link to create a new password, and the password_required? method will allow us to create a user without providing a password.
Go try it out. Create a user, and then check your email for a link, which should let you create a new password, and log you into the sytem.
Step 3 – Projects
We are going to create a simple Project Management system. Not anything too complicated though, just something that will let us manage projects and tasks for the project, and assign tasks to certain users. First thing, is to create a project model:
|
1
|
rails generate model Project title:string
|
Active Admin relies on Rails’ models for validation, and we don’t want projects with no title, so let’s add some validations to the generated model:
|
1
2
|
# rails
validates :title, :presence => true
|
Now, we need to generate an Active Admin resource, by running:
|
1
|
rails generate active_admin:resource Project
|
For now, that’s all we need for projects. After migrating your database, take a look at the interface that you just created. Creating a project with no title fails, which is what we expected. See how much you accomplished with just a few lines of code?
Step 4 – Tasks
Projects aren’t very useful without tasks right? Let’s add that:
|
1
|
rails generate model Task project_id:integer admin_user_id:integer title:string is_done:boolean due_date:date
|
This creates a task model that we can associate with projects and users. The idea is that a task is assigned to someone and belongs to a project. We need to set those relations and validations in the model:
|
1
2
3
4
5
6
7
|
class Task < ActiveRecord::Base
belongs_to :project
belongs_to :admin_user
validates :title, :project_id, :admin_user_id, :presence => true
validates :is_done, :inclusion => { :in => [true, false] }
end
|
Remember to add the relations to the Project and AdminUser models as well:
|
1
2
3
4
5
|
class AdminUser < ActiveRecord::Base
has_many :tasks
# ...
end
|
|
1
2
3
4
5
|
class Project < ActiveRecord::Base
has_many :tasks
# ...
end
|
Migrate the database, and register the task model with Active Admin:
|
1
|
rails generate active_admin:resource Task
|
Now go and take a look at the tasks panel in your browser. You just created a project management system! Good job.
Step 5 – Making It Even Better
The system we just created isn’t too sophisticated. Luckily, Active Admin is not just about creating a nice scaffolding system, it gives you far more power than that. Let’s start with the Projects section. We don’t really need the id, created and updated columns there, and we certainly don’t need to be able to search using those columns. Let’s make that happen:
|
1
2
3
4
5
6
7
8
9
10
|
index do
column :title do |project|
link_to project.title, admin_project_path(project)
end
default_actions
end
# Filter only by title
filter :title
|
A few notes here:
- When you specify columns, you can customize what is printed on every row. Simply pass a block with an argument to it, and return whatever you want in there. In this case, we are printing a link to the project’s detail page, which is easier than clicking the “View” link on the right.
- The filters on the right are also customizable. Just add a call to
filter for every column you want to be able to filter with.
The project’s detail page is a little boring don’t you think? We don’t need the date columns and the id here, and we could show a list of the tasks more directly. Customizing the detail page is accomplished by using the show method in the app/admin/projects.rb file:
|
1
2
3
4
5
6
7
8
9
10
|
show :title => :title do
panel "Tasks" do
table_for project.tasks do |t|
t.column("Status") { |task| status_tag (task.is_done ? "Done" : "Pending"), (task.is_done ? k : :error) }
t.column("Title") { |task| link_to task.title, admin_task_path(task) }
t.column("Assigned To") { |task| task.admin_user.email }
t.column("Due Date") { |task| task.due_date? ? l(task.due_date, :format => :long) : '-' }
end
end
end
|
Let’s walk through the code:
show :title => :title specifies the title the page will have. The second :title specifies the model’s column that will be used.
- By calling
panel "Tasks" we create a panel with the given title. Within it, we create a custom table for the project’s tasks, using table_for.
- We then specify each column and the content’s it should have for each row.
- The “Status” column will contain “Done” or “Pending” whether the task is done or not.
status_tag is a method that renders the word passed with a very nice style, and you can define the color by passing a second argument with either : ok, :warning and :error for the colors green, orange and red respectively.
- The “Title” column will contain a link to the task so we can edit it.
- The “Assigned To” column just contains the email of the person responsible.
- The “Due Date” will contain the date the task is due, or just a “-” if there’s no date set.
Step 6 – Some Tweaks for the Tasks
How about an easy way to filter tasks that are due this week? Or tasks that are late? That’s easy! Just use a scope. In the tasks.rb file, add this:
|
1
2
3
4
5
6
7
8
9
10
|
scope :all, :default => true
scope :due_this_week do |tasks|
tasks.where('due_date > ? and due_date < ?', Time.now, 1.week.from_now)
end
scope :late do |tasks|
tasks.where('due_date < ?', Time.now)
end
scope :mine do |tasks|
tasks.where(:admin_user_id => current_admin_user.id)
end
|
Let’s review that code:
scope :all defines the default scope, showing all rows.
scope accepts a symbol for the name, and you can pass a block with an argument. Inside the block you can refine a search according to what you need. You can also define the scope inside the model and simply name it the same as in this file.
- As you can see, you can access the current logged in user’s object using
current_admin_user.
Check it out! Just above the table, you’ll see some links, which quickly show you how many tasks there are per scope, and lets you quickly filter the list. You should further customize the table and search filters, but I’ll leave that task to you.
We’re now going to tweak the task’s detail view a bit, since it looks rather cluttered:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
show do
panel "Task Details" do
attributes_table_for task do
row("Status") { status_tag (task.is_done ? "Done" : "Pending"), (task.is_done ? k : :error) }
row("Title") { task.title }
row("Project") { link_to task.project.title, admin_project_path(task.project) }
row("Assigned To") { link_to task.admin_user.email, admin_admin_user_path(task.admin_user) }
row("Due Date") { task.due_date? ? l(task.due_date, :format => :long) : '-' }
end
end
active_admin_comments
end
|
This will show a table for the attributes of the model (hence the method’s name, attributes_table_for). You specify the model, in this case task, and in the block passed, you define the rows you want to show. It’s roughly the same we defined for the project’s detail page, only for the task. You may be asking yourself: What’s that “active_admin_comments” method call for? Well, Active Admin provides a simple commenting system for each model. I enabled it here because commenting on a task could be very useful to discuss functionality, or something similar. If you don’t call that method, comments will be hidden.
There’s another thing I’d like to show when viewing a task’s detail, and that’s the rest of the assignee’s tasks for that project. That’s easily done using sidebars!
|
1
2
3
4
5
6
|
sidebar "Other Tasks For This User", nly => :show do
table_for current_admin_user.tasks.where(:project_id => task.project) do |t|
t.column("Status") { |task| status_tag (task.is_done ? "Done" : "Pending"), (task.is_done ? k : :error) }
t.column("Title") { |task| link_to task.title, admin_task_path(task) }
end
end
|
This creates a sidebar panel, titled “Other Tasks For This User”, which is shown only on the “show” page. It will show a table for the currentadminuser, and all tasks where the project is the same as the project being shown (you see, task here will refer to the task being shown, since it’s a detail page for one task). The rest is more or less the same as before: some columns with task details.
Step 7 – The Dashboard
You may have noticed, when you first launched your browser and logged into your app, that there was a “Dashboard” section. This is a fully customizable page where you can show nearly anything: tables, statistics, whatever. We’re just going to add the user’s task list as an example. Open up the dashboards.rb file and revise it, like so:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
ActiveAdmin::Dashboards.build do
section "Your tasks for this week" do
table_for current_admin_user.tasks.where('due_date > ? and due_date < ?', Time.now, 1.week.from_now) do |t|
t.column("Status") { |task| status_tag (task.is_done ? "Done" : "Pending"), (task.is_done ? k : :error) }
t.column("Title") { |task| link_to task.title, admin_task_path(task) }
t.column("Assigned To") { |task| task.admin_user.email }
t.column("Due Date") { |task| task.due_date? ? l(task.due_date, :format => :long) : '-' }
end
end
section "Tasks that are late" do
table_for current_admin_user.tasks.where('due_date < ?', Time.now) do |t|
t.column("Status") { |task| status_tag (task.is_done ? "Done" : "Pending"), (task.is_done ? k : :error) }
t.column("Title") { |task| link_to task.title, admin_task_path(task) }
t.column("Assigned To") { |task| task.admin_user.email }
t.column("Due Date") { |task| task.due_date? ? l(task.due_date, :format => :long) : '-' }
end
end
end
|
The code should be fairly familiar to you. It essentially creates two sections (using the section method and a title), with one table each, which displays current and late tasks, respectively.
Conclusion
We’ve created an extensive application in very few steps. You may be surprised to know that there are plenty more features that Active Admin has to offer, but it’s not possible to cover them all in just one tutorial, certainly. If you’re interested in learning more about this gem, visit activeadmin.info.
You also might like to check out my project, called active_invoices on GitHub, which is a complete invoicing application made entirely with Active Admin. If you have any questions, feel free to ask them in the comments, or send me a tweet.
Showing 26 comments
yes, keep spreading the virtualenv love, nice article.
great stuff! thanks for putting the work in and sharing this informative piece.
Absolutely! I love putting these together – let me know if you have any other walkthroughs/screencasts you’d like to see.
Question: What happens to my $PYTHONPATH within virtualenv?
echo $PYTHONPATH
returns nothing. Is it now assumed that all directories within the current virtualenv are automatically on the path? Looks like it – I had to comment out the $PYTHONPATH lines in my .bash_profile to get things basically working.
But that doesn’t seem to be the whole enchilada. I was able to pip install 3rd party apps with pip or through the requirements file, but I also have a lot of custom reusable apps with no setup.py. I can’t install them with pip, and if I put them manually in /src, /lib, or /include, my Django project fails to see that they’re present. They worked previously, when I was basing everything off the $PYTHONPATH, but now there is no pythonpath.
In general, what directories within the virtualenv are recommended for what?
Side question: How would you switch to using a different version of an installed package (such as Django?)
Thanks.
Hi shacker – I’m not sure what’s happening to your PYTHONPATH. The author’s code preserves the existing PYTHONPATH (see line 306 athttp://bitbucket.org/ianb/virt…. I personally only use virtual python environments and virtualenv’s –no-site-packages option so I don’t explicity set PYTHONPATH in my profile and haven’t run into this.
To answer your second question, if you need a package that doesn’t have a setup.py, you can take two approaches. Put the package directly in lib/python2.x/site-packages/ or use the add2virtualenv command, which creates a .pth file in lib/python2.x/site-packages/.
A virtual environment creates an isolated folder with bin, lib, include, etc. It’s similar to your system folders – executable scripts are in bin, lib contains python2.x, etc.
If you wanted to change the version of an installed package, you can run pip install -U Django==X, where X is the version of Django (or any other package) you want to modify.
Hope that helps!
Thanks for the response Rich. I think I confused things a bit. Yes, if I keep a $PYTHONPATH set in bash, it’s preserved in the virtualenv. I have commented out the path in .bash_profile. What I *thought* would happen was that when I entered the virtualenv I’d get a new, automatically created $PYTHONPATH that reflected the new environment. Apparently not.
Interestingly, even though echo pythonpath shows nothing, Django debug pages still show a full pythonpath reflecting the virtualenv. So it’s there, but the shell doesn’t know about it.
The import errors turned out to be my own mistake (tangled package paths only revealed when I started isolating things with virtualenv).
Thanks again.
Good intro, Rich. Btw, is there a way to change the virtualenv prompt?
Just in case anyone else ran into the same problem, I couldn’t find virtualenvwrapper_bashrc in /usr/local/bin/ on my OS X 10.5 machine, and turns out it was here, so I symlinked it from here and it worked:
ln -s /Library/Frameworks/Python.framework/Versions/2.6/bin/virtualenvwrapper_bashrc /usr/local/bin
If yours isn’t there, (from bash shell) just do a:
find / -name virtualenvwrapper_bashrc
(for the record I installed virtualenv and virtualenvwrapper with pip)
You can change the virtual environment’s prompt using the –prompt option to virtualenv:
–prompt==PROMPT Provides an alternative prompt prefix for this environment
So, where do you keep your real apps?
Can you put a bit of code on how you develop your app in this framework?
Let’s say you are making a site called mySite that has app1 app2 & app3.
I primarily use Django and all of my projects live outside the virtualenv completely. It’s the packages that my projects use that live in the virtualenv.
For example, let’s say I have a site called mycoolsite.com. I’ll create a virtualenv called mycoolsite. The actual project files and web server configurations are in a repo and are checked out somewhere (e.g. /var/www/mysite/code/. The web server configs pull everything together. In the instance of a Django project, I’d have a .wsgi file that sets up my paths and points to my project files.
A lot of updates have happened to virtualenvwrapper since I wrote this post. It’s definitely time for me to update and show off the new functionality.
Hi Rich,
So, if I used the add2virtualenv, then I decide that I want to remove it, how do I go about it?
I am using buildout right now, it is powerful, but it has its own problems. I am trying to move over to virenv. Any suggestions for the new converts?
Hi there – honestly I don’t use add2virtualenv at all. I just highlighted it because it’s available. But what it does is just put a .pth file inside site-packages. So you could just remove that file.
My preferred way of working is to use the “–no-site-packages” flag to mkvirtualenv. This creates a clean site-packages folder and doesn’t pull in system-wide site-packages. Then you have a nice, clean env to start with, installing all the pieces you need.
Also – it’s interesting to me that lots of people are coming from buildout to ve. I’m doing the exact opposite at my new job, although I could see that changing at some point.
I still like buildout for a “finished” product. I wish it had the freeze option so I didn’t have to use the version tag and manually lock up all working versions. But lets say that I want to quickly test a new django CMS that someone wrote. With buidout, I have to go about and creating a buildout env to get it tested. With virtualenv, in few short minutes, I have the new CMS/example app up and running. I’d think that if buildout and virtualevn marry someday, they would produce a beautiful offspring.
I’m confused. If virtualenvwrapper maintains your virtual environments, then it can’t reside inside any of those virtual environments where *does* it live?
Hi Ryan – you run “sudo easy_install virtualenv virtualenvwrapper”. It lives in your system Python’s site-packages.
You can also add a hook to your workon home directory, just like you do with each individual virtualenv in the bin directory.
Just create the hook file in your workon home (such as “postactivate”) and it will be applied when ANY virtualenv is called.
For example, you could create a hook in your workon home called “postactivate” and then add in:
cdvirtualenv
…to the file and any time a virtualenv is activated, it will automatically cd to the directory of your virtualenv. This makes it so you only need to declare your hooks once, not one time for each virtualenv.
Good write up!
Cheers,
Dana W.
This is a great write up, thanks.
I always keep my projects outside of the virtual environments and they live in a separate directory.
To keep things simple, I create a symlink to the virtual environments python inside my project.
ln -s /srv/environments/djnonrel/bin/python /srv/projects/myproject/python
Then you can just do something like this in myproject:
./python manage.py runserver
etc
And just forget about where you put the environment while you work on your project
Thanks for writing this up and the screencast. Coming from an acolyte’s level of experience with ruby, I eagerly anticipated something like rvm for python. It looks like virtualenvwrapper will serve that role well. What’s really fantastic about this environment tools is how easy it makes setting up new apps or systems.
Thanks. BTW your link to “Doug’s blog post” is missing.
Hope you found it helpful! Fixed that link – thanks for pointing it out.
I use closed environments for each projects with freeze versions, and I wonder how you supervise your packages deployed, to check outdated packages for example.
Is there any supervision tool, app to do this?
Hi Oliver – a colleague of mine, Corey Oordt, wrote some Fabric tools to check for outdated/out of sync dependencies: https://gist.github.com/106622…. That may be what you’re looking for.
Thanks, I have still have a look on, It’s pretty my needs, but I am not responsible of deployment, so I don’t have any ssh access… anyway, should be easy to port for webby approach!
Nice writeup, I’ve been linking to this in all my github project installation instructions.
The filename virtualenvwrapper_bashrc has been updated since prior posts, for version 3.0 of virtualenvwrapper the filename is virtualenvwrapper.sh