| 1 |
Rails AutoAdmin Plugin |
|---|
| 2 |
|
|---|
| 3 |
What is it? |
|---|
| 4 |
|
|---|
| 5 |
A plugin for Ruby on Rails that automagically creates an administration |
|---|
| 6 |
interface, based on your models. It is heavily inspired by the Django |
|---|
| 7 |
[1] administration system, and the only theme currently available is |
|---|
| 8 |
based directly on Django's administration system. From the screenshots |
|---|
| 9 |
posted so far, it appears to share goals with Streamlined [2]. |
|---|
| 10 |
|
|---|
| 11 |
[1] Django: http://www.djangoproject.com/ |
|---|
| 12 |
[2] Streamlined: http://streamlined.relevancellc.com/ |
|---|
| 13 |
|
|---|
| 14 |
|
|---|
| 15 |
Example? |
|---|
| 16 |
|
|---|
| 17 |
class Customer < ActiveRecord::Base |
|---|
| 18 |
belongs_to :store |
|---|
| 19 |
has_many :payments, :order => 'payment_date DESC' |
|---|
| 20 |
|
|---|
| 21 |
def name; first_name + ' ' + last_name; end |
|---|
| 22 |
|
|---|
| 23 |
sort_by :last_name |
|---|
| 24 |
search_by :first_name, :last_name |
|---|
| 25 |
filter_by :active, :store |
|---|
| 26 |
default_filter :active => true |
|---|
| 27 |
list_columns :store, :first_name, :last_name |
|---|
| 28 |
|
|---|
| 29 |
admin_fieldset do |b| |
|---|
| 30 |
b.text_field :first_name |
|---|
| 31 |
b.text_field :last_name |
|---|
| 32 |
b.select :store |
|---|
| 33 |
end |
|---|
| 34 |
admin_child_table 'Payments', :payments do |b| |
|---|
| 35 |
b.static_text :payment_date |
|---|
| 36 |
b.static_text :amount |
|---|
| 37 |
end |
|---|
| 38 |
end |
|---|
| 39 |
|
|---|
| 40 |
Results in: |
|---|
| 41 |
http://trebex.net/~matthew/auto-admin-0.0/list.png |
|---|
| 42 |
http://trebex.net/~matthew/auto-admin-0.0/edit.png |
|---|
| 43 |
|
|---|
| 44 |
|
|---|
| 45 |
What isn't it? |
|---|
| 46 |
|
|---|
| 47 |
Scaffolding. This is not a view generator for you to then customise. |
|---|
| 48 |
Either it provides the interface you want, or it doesn't. (With a |
|---|
| 49 |
limited, but hopefully expanding, set of exceptions.) |
|---|
| 50 |
|
|---|
| 51 |
For everyone. This is for applications that have a public interface and |
|---|
| 52 |
a restricted-access administrative interface. Its goal is not to |
|---|
| 53 |
generate views you would otherwise have to craft manually, so much as |
|---|
| 54 |
generating views you otherwise wouldn't bother to create. Of course, a |
|---|
| 55 |
neat side-effect of using this is that your boss (or your client's IT |
|---|
| 56 |
manager) can make simple database-level changes that would otherwise |
|---|
| 57 |
require a developer to use either the console or direct SQL. If you're |
|---|
| 58 |
trying to create an interface for all your users, this probably isn't |
|---|
| 59 |
for you. |
|---|
| 60 |
|
|---|
| 61 |
|
|---|
| 62 |
Where is it? |
|---|
| 63 |
|
|---|
| 64 |
Right now, there's just a tarball available at |
|---|
| 65 |
http://trebex.net/~matthew/auto-admin-0.0/auto-admin-0.0.tar.gz. |
|---|
| 66 |
|
|---|
| 67 |
I need to get a public SVN repository set up for it, and populate a |
|---|
| 68 |
useful Web page. Writing this has at least given me some material to |
|---|
| 69 |
that end. |
|---|
| 70 |
|
|---|
| 71 |
|
|---|
| 72 |
Is it usable? |
|---|
| 73 |
|
|---|
| 74 |
Perhaps, but probably not quite yet. It currently doesn't like editable |
|---|
| 75 |
sublists, for one, and it lacks a reliable set of tests... I've TDDed a |
|---|
| 76 |
few features, but the tests covering the rest of the functionality are |
|---|
| 77 |
rather sparse. |
|---|
| 78 |
|
|---|
| 79 |
I'm releasing mostly for selfish reasons: I'm hoping that publishing the |
|---|
| 80 |
code will shame me into fixing the nasty bits. :) |
|---|
| 81 |
|
|---|
| 82 |
Other, more pressing, time constraints are forcing this release before I |
|---|
| 83 |
get it cleaned up as much as I'd like (hence the lack of public SVN or a |
|---|
| 84 |
website). An unfortunate side-effect of this is the timing with respect |
|---|
| 85 |
to Streamlined's release. On that note, I'll be looking closely at |
|---|
| 86 |
Streamlined upon its release, probably with a view to moving any useful |
|---|
| 87 |
functionality I've built here into it; Justin and Stuart have far more |
|---|
| 88 |
Rails experience than I do, and I expect that fact will be heavily |
|---|
| 89 |
reflected in any comparison between this plugin and Streamlined. |
|---|
| 90 |
|
|---|
| 91 |
|
|---|
| 92 |
What does it assume? |
|---|
| 93 |
|
|---|
| 94 |
All objects it encounters can be usefully represented to a human as a |
|---|
| 95 |
string. It achieves this by adding a to_label method to Object, which |
|---|
| 96 |
will return the first available of (label, name, to_s, or inspect). |
|---|
| 97 |
|
|---|
| 98 |
Your access control requirements for the administration section are |
|---|
| 99 |
relatively "all or nothing". I intend to add simple class- and fieldset- |
|---|
| 100 |
level declarative permission checking soonish (whenever I start to need |
|---|
| 101 |
it). Access control based on querying individual objects should come at |
|---|
| 102 |
some point, but I don't anticipate needing that level of control any |
|---|
| 103 |
time soon. You can currently customise which fields are displayed (the |
|---|
| 104 |
field list is a block of code, after all), but will end up with empty |
|---|
| 105 |
fieldsets if you don't include any. |
|---|
| 106 |
|
|---|
| 107 |
If you have any access control (which I expect will pretty much always |
|---|
| 108 |
be the case), you must have a User constant, it must respond to one of |
|---|
| 109 |
(authenticate, login, or find_by_username_and_password), and that method |
|---|
| 110 |
must take two strings, and return nil for failure or a non-false value |
|---|
| 111 |
for success. It *should* return the authenticated user's model -- if the |
|---|
| 112 |
returned value responds to one or more of (active?, enabled?, disabled?, |
|---|
| 113 |
and admin?), they will be treated appropriately. The currently logged-in |
|---|
| 114 |
user (as returned by the authentication function) will be looked for and |
|---|
| 115 |
stored in session[:user], so if other parts of your site do the same, |
|---|
| 116 |
things will Just Work. I'm concious of the fact that storing |
|---|
| 117 |
ActiveRecord instances in the session is inadvisable, and will probably |
|---|
| 118 |
change this sometime soon. |
|---|
| 119 |
|
|---|
| 120 |
|
|---|
| 121 |
How do I use it? |
|---|
| 122 |
|
|---|
| 123 |
Initially (after installing the plugin, obviously), you need to add a |
|---|
| 124 |
few lines to the bottom of your environment.rb: |
|---|
| 125 |
AutoAdmin.config do |admin| |
|---|
| 126 |
# This information is used by the theme to construct a useful |
|---|
| 127 |
# header; the first parameter is the full URL of the main site, the |
|---|
| 128 |
# second is the displayed name of the site, and the third (optional) |
|---|
| 129 |
# parameter is the title for the administration site. |
|---|
| 130 |
admin.set_site_info 'http://www.example.tld/', 'example.tld' |
|---|
| 131 |
|
|---|
| 132 |
# "Primary Objects" are those for which lists should be directly |
|---|
| 133 |
# accessible from the home page. |
|---|
| 134 |
admin.primary_objects = %w(actor film user) |
|---|
| 135 |
|
|---|
| 136 |
admin.theme = :django # Optional; this is the default. |
|---|
| 137 |
end |
|---|
| 138 |
|
|---|
| 139 |
Having done that, you can now (re-)start script/server, and navigate to |
|---|
| 140 |
http://localhost:3000/admin/. Yes, it installs its own routes. Yes, |
|---|
| 141 |
they're hard-coded. Yes, that needs to change... for now, just don't try |
|---|
| 142 |
to use /admin/ for anything else. :) |
|---|
| 143 |
|
|---|
| 144 |
To customise which fields appear in the edit and list screens, you go on |
|---|
| 145 |
to... |
|---|
| 146 |
|
|---|
| 147 |
|
|---|
| 148 |
How does it work? - Part I, Declarative UI definition |
|---|
| 149 |
|
|---|
| 150 |
The plugin adds a number of singleton methods to ActiveRecord::Base, |
|---|
| 151 |
which permit you to declare how the administration interface should |
|---|
| 152 |
behave. |
|---|
| 153 |
|
|---|
| 154 |
This set of methods, which are quite central to the utility of the |
|---|
| 155 |
plugin, have grown rather organically, over a period of time (as has my |
|---|
| 156 |
Ruby-fu). I've attempted to clear out the most glaring API |
|---|
| 157 |
inconsitencies, but it's still a bit of a mess. Some of the |
|---|
| 158 |
implementations definitely leave a bit to be desired. Cleaning this up |
|---|
| 159 |
is near the top of my TODO list. That said, it should all work. :) |
|---|
| 160 |
|
|---|
| 161 |
I really need to go through and write decent documentation for all the |
|---|
| 162 |
published methods, but for now, the following summary should at least |
|---|
| 163 |
act as a guide. Essentially, inside the model, you can use the following |
|---|
| 164 |
methods: |
|---|
| 165 |
object_group(group_name) |
|---|
| 166 |
# Declares which 'object group' this object belongs to, for use in |
|---|
| 167 |
# the interface. Currently, this is used to group together related |
|---|
| 168 |
# objects on the index page. |
|---|
| 169 |
sort_by(column, reverse=false) |
|---|
| 170 |
# Instructs the list view to sort on the specified column by |
|---|
| 171 |
# default. |
|---|
| 172 |
search_by(*columns) |
|---|
| 173 |
# Add rudimentary text searching across the named columns. Note that |
|---|
| 174 |
# this defines a MyModel.search(many, query, options={}) wrapper |
|---|
| 175 |
# around MyModel.find(many, options). |
|---|
| 176 |
filter_by(*columns) |
|---|
| 177 |
# Allow filtering of the list screen by the named columns (filtering |
|---|
| 178 |
# currently works for: custom, boolean, date, belongs_to, has_one, |
|---|
| 179 |
# and string). Note that the last three will do rather nasty and |
|---|
| 180 |
# sub-optimal queries to determine the filter options. |
|---|
| 181 |
default_filter(filters) |
|---|
| 182 |
# Takes a hash of (column, value) pairs, to default a filter to |
|---|
| 183 |
# something other than 'All'. |
|---|
| 184 |
filter_options_for(column, choices, &block) |
|---|
| 185 |
# Specifies a fixed set of choices to be offered as filter options |
|---|
| 186 |
# instead of automatically working it out. Choices should be a |
|---|
| 187 |
# (value, label) hash. The optional block will be given each value |
|---|
| 188 |
# in turn, and should return an SQL condition fragment. |
|---|
| 189 |
column_labels(labels) |
|---|
| 190 |
# Takes a hash of (column, label) pairs, to change the default label |
|---|
| 191 |
# for a field to explicitly define the human label for a column. |
|---|
| 192 |
# This label will be the default used in both list and edit views. |
|---|
| 193 |
list_columns(*columns, &proc) |
|---|
| 194 |
# Takes either a simple-list of column names, or a Field Definition |
|---|
| 195 |
# Block (see below) |
|---|
| 196 |
admin_fieldset(label='', *columns, &proc) |
|---|
| 197 |
# Defines a fieldset for edit views. For simple use, you can just |
|---|
| 198 |
# give it a list of columns. Once you get started, you'll want to |
|---|
| 199 |
# pass a Field Definition Block, though. |
|---|
| 200 |
admin_child_table(label, collection, options={}, &proc) |
|---|
| 201 |
# Defines a fieldset for edit views, to show a table of items from a |
|---|
| 202 |
# child collection. It uses a Field Definition Block to declare what |
|---|
| 203 |
# columns should be shown. Generally, you'd want to use the |
|---|
| 204 |
# static_text helper, I suspect. |
|---|
| 205 |
# WARNING: This has no tests, and I'm almost certain it will break |
|---|
| 206 |
# horribly if you try to use anything other than static_text. |
|---|
| 207 |
admin_child_form(collection, options={}, &proc) |
|---|
| 208 |
# Defines a "fieldset" for edit views, to show *several* fieldsets, |
|---|
| 209 |
# each containing one object from a child collection. It uses a |
|---|
| 210 |
# Field Definition Block to declare what columns should be shown. |
|---|
| 211 |
# I don't think it'd be wise to use this on a large collection, but |
|---|
| 212 |
# it's your application. :) |
|---|
| 213 |
# WARNING: This also has no tests, and I believe it will break |
|---|
| 214 |
# horribly if you try to use it at all. |
|---|
| 215 |
|
|---|
| 216 |
Field Definition Block?!? |
|---|
| 217 |
|
|---|
| 218 |
A number of the above methods provide for a block to declare what fields |
|---|
| 219 |
are to be shown. This is achieved by yielding a builder to the block. |
|---|
| 220 |
Depending on context, the mood of a theme author, and the phase of the |
|---|
| 221 |
moon, a given block will see several builders in its lifetime. Not all |
|---|
| 222 |
builders will have an active object; all will respond to the +object+ |
|---|
| 223 |
method, though. A basic field definition block will just call a field |
|---|
| 224 |
helper on the builder for each field that it wishes to display. The |
|---|
| 225 |
+auto_field+ helper (which automatically determines an appropriate field |
|---|
| 226 |
type based on column and association metadata) is available if you only |
|---|
| 227 |
want to specify the field type for some of the fields. All field helpers |
|---|
| 228 |
take (field_name, options={}, *other_stuff). Most just take the two |
|---|
| 229 |
parameters, and I'm considering deprecating the extra parameters on |
|---|
| 230 |
those that currently support them. Note that unlike a standard builder, |
|---|
| 231 |
you don't have to do anything with the return value; the theme's actual |
|---|
| 232 |
FormBuilder is wrapped by a DeclarativeFormBuilder, which takes care of |
|---|
| 233 |
that for you. |
|---|
| 234 |
|
|---|
| 235 |
In theory, there's no compelling reason you can't add complex logic to a |
|---|
| 236 |
field definition block, such as examining the current user, or even the |
|---|
| 237 |
builder's active object (though I strongly encourage you to handle nil |
|---|
| 238 |
permissively, at this stage). It would be unwise to vary the fields |
|---|
| 239 |
returned based on the object for a list view, for fairly obvious |
|---|
| 240 |
reasons. |
|---|
| 241 |
|
|---|
| 242 |
Available Form Helpers |
|---|
| 243 |
|
|---|
| 244 |
Simple helpers that just delegate to the ActionView's FormBuilder: |
|---|
| 245 |
hidden_field, date_select, datetime_select, text_field, text_area, |
|---|
| 246 |
check_box |
|---|
| 247 |
|
|---|
| 248 |
+select+ and +radio_group+ operate in basically the same way; they both |
|---|
| 249 |
provide a method of selecting one out of several choices (ignoring |
|---|
| 250 |
select :multiple, that is). Note that select's list of choices, normally |
|---|
| 251 |
the second parameter to the select helper, has been relegated to a |
|---|
| 252 |
:choices entry in the options, for API consistency. |
|---|
| 253 |
|
|---|
| 254 |
+static_text+ just outputs an HTML-escaped string representation of the |
|---|
| 255 |
field's value. It is useful both for read-only fields in forms, and as |
|---|
| 256 |
the primary helper in lists. |
|---|
| 257 |
|
|---|
| 258 |
+auto_field+, as discussed above, will automatically select a suitable |
|---|
| 259 |
field helper, based on the column and association metadata. Where there |
|---|
| 260 |
are multiple suitable candidates, it tries to go for the more |
|---|
| 261 |
generally-applicable choice (for example, it favours a +select+ over a |
|---|
| 262 |
+radio_group+ for a belongs_to association). |
|---|
| 263 |
|
|---|
| 264 |
None of the following actually work, but they're defined, waiting for me |
|---|
| 265 |
to come back and write them. +html_area+ will eventually use FCKeditor |
|---|
| 266 |
by default, and presumably the file/image fields will delegate to |
|---|
| 267 |
file_column. |
|---|
| 268 |
html_area, hyperlink, file_field, image_field, static_image, |
|---|
| 269 |
static_file, static_html |
|---|
| 270 |
|
|---|
| 271 |
|
|---|
| 272 |
How does it work? - Part II, Themes |
|---|
| 273 |
|
|---|
| 274 |
The theme bundled with the plugin is named 'django'; all credit for its |
|---|
| 275 |
excellent appearance goes to the Django project. I hope we can get a |
|---|
| 276 |
couple of standard themes, but they won't be coming from me... |
|---|
| 277 |
experience shows that I shouldn't try to make things look good. I |
|---|
| 278 |
believe I've successfully drawn lines in all the right places for what |
|---|
| 279 |
is in the plugin's core, and what's in a theme. I've already developed |
|---|
| 280 |
most of a second theme (which will not be released) for my employer, so |
|---|
| 281 |
the infrastructure is mostly proven. A more coherent HOWTO on creating |
|---|
| 282 |
themes (which can just be installed as seperate Rails plugins, then |
|---|
| 283 |
selected in environment.rb) will be forthcoming Real Soon Now, though |
|---|
| 284 |
this section has ended up covering most of the basics. |
|---|
| 285 |
|
|---|
| 286 |
The 30 second summary -- a theme comprises: |
|---|
| 287 |
FormBuilder (subclass of AutoAdminSimpleTheme::FormBuilder), to create |
|---|
| 288 |
an Edit screen (a real form) |
|---|
| 289 |
|
|---|
| 290 |
TableBuilder (subclass of AutoAdmin::TableBuilder(FormBuilder)), to |
|---|
| 291 |
create a List screen (a creative interpretation of "form", which seems |
|---|
| 292 |
to map surprisingly well, for now). |
|---|
| 293 |
|
|---|
| 294 |
FormProcessor (subclass of AutoAdminSimpleTheme::FormProcessor), which |
|---|
| 295 |
implements the same set of helper methods as the FormBuilders, but |
|---|
| 296 |
instead of returning HTML, its job is to perform any transformations |
|---|
| 297 |
on the params hash to correspond with unusual form field |
|---|
| 298 |
representations -- the base FormProcessor transforms keys referencing |
|---|
| 299 |
associations to reference the underlying columns (actor -> actor_id), |
|---|
| 300 |
for example. This class will often be empty, especially once I provide |
|---|
| 301 |
a facility with which to inject custom field helpers (for composed_of |
|---|
| 302 |
and maybe some belongs_to, mostly) into the base builder and |
|---|
| 303 |
processor. |
|---|
| 304 |
|
|---|
| 305 |
A complete set of views, including a layout, which delegate the hard |
|---|
| 306 |
work to the FormBuilders. |
|---|
| 307 |
|
|---|
| 308 |
A 'public' directory, containing any required image, javascript, and |
|---|
| 309 |
stylesheet assets. |
|---|
| 310 |
|
|---|
| 311 |
A wrapper module, AutoAdmin#{name}Theme, which is responsible for: |
|---|
| 312 |
* Containing the FormBuilders and FormProcessor |
|---|
| 313 |
* Returning the full filesystem path to the 'views' and 'public' |
|---|
| 314 |
directories |
|---|
| 315 |
* Returing any theme-specific helpers, for injection into the |
|---|
| 316 |
controller |
|---|
| 317 |
* Injecting any theme-specific includes for ActiveRecord::Base |
|---|
| 318 |
(I've proven this to be possible, though can't think of a sane |
|---|
| 319 |
reason a theme would want to do so) |
|---|
| 320 |
|
|---|
| 321 |
Extending your theme module with AutoAdmin::ThemeHelpers will help to |
|---|
| 322 |
keep the module fairly DRY; it provides a +helper+ method, which can be |
|---|
| 323 |
given a list of modules and/or a block, and directs the 'view_directory' |
|---|
| 324 |
and 'asset_root' methods to a directory(*subdirs) singleton method, |
|---|
| 325 |
which you must define -- presumably using __FILE__. |
|---|
| 326 |
|
|---|
| 327 |
NB: For good reasons that I can't remember right now, a couple of helper |
|---|
| 328 |
methods have APIs that don't match the standard Rails FormBuilder, |
|---|
| 329 |
despite matching names. The one that comes to mind is +select+ -- the |
|---|
| 330 |
choices have been moved into the options hash, to keep all method |
|---|
| 331 |
signatures of the form (field_name, options, *other_stuff). |
|---|
| 332 |
|
|---|
| 333 |
|
|---|
| 334 |
What's planned, but missing? |
|---|
| 335 |
|
|---|
| 336 |
The ability for the application to inject custom field types into the |
|---|
| 337 |
base FormBuilder and FormProcessor. The theme-specific versions of these |
|---|
| 338 |
classes are available so that, for example, a theme can decide how a |
|---|
| 339 |
date_field should be presented, and can correspondingly recover the |
|---|
| 340 |
values from multiple inputs... they don't map as well to an |
|---|
| 341 |
application's requirement for a 'currency' field. Of course, there's |
|---|
| 342 |
nothing stopping an application re-opening the classes and adding an |
|---|
| 343 |
appropriate helper method to each... there's just a bit of undesirable |
|---|
| 344 |
complexity involved if you want auto_field to detect and use it (which |
|---|
| 345 |
suggests to me that auto_field needs a bit of a rethink). |
|---|
| 346 |
|
|---|
| 347 |
A way for the application to reliably extend the AutoAdminController, |
|---|
| 348 |
and add appropriate views somewhere, for those occasions when you have a |
|---|
| 349 |
couple of screens that need to be hand-crafted, such as a statistics |
|---|
| 350 |
display, or a particular edit screen that needs a specialised workflow. |
|---|
| 351 |
Note that if you feel this constraint too much, you're probably pushing |
|---|
| 352 |
the plugin into a role it doesn't fit. |
|---|
| 353 |
|
|---|
| 354 |
Simple methods allowing an application to add navigation options, and |
|---|
| 355 |
perhaps the ability to insert Components into the "dashboard" on the |
|---|
| 356 |
index page? |
|---|
| 357 |
|
|---|
| 358 |
A top-level "menu", containing links to the primary object lists by |
|---|
| 359 |
default, that a theme can permanently display. |
|---|
| 360 |
|
|---|
| 361 |
It's probably a better idea to store the logged-in user's id, instead of |
|---|
| 362 |
the user object, in the session. |
|---|
| 363 |
|
|---|
| 364 |
|
|---|
| 365 |
Longer-term architectural considerations? |
|---|
| 366 |
|
|---|
| 367 |
After starting off defining the administration interfaces directly in |
|---|
| 368 |
the models (as Django does), I was strongly considering moving them all |
|---|
| 369 |
into an application-specific controller, that would subclass |
|---|
| 370 |
AutoAdminController. I haven't gotten around to doing that, and am now |
|---|
| 371 |
quite intruiged by the approach taken by Streamlined -- adding a new |
|---|
| 372 |
type of class. Any such move is primarily aimed at solving a problem I'm |
|---|
| 373 |
not yet sufferring, though, so for now it's just a topic to ponder. |
|---|
| 374 |
|
|---|
| 375 |
|
|---|
| 376 |
|
|---|
| 377 |
|
|---|