I've been Routes-ing away happily with my application, using a custom
integration with CherryPy. In converting my application to Routes, I've
noticed this usage pattern:
We have certain objects that have cannonical URLs, for example, all
"Person" object might have a unique, nice human radable form like:
"/person/john-smith-123". For the route, I might have something like
this: connect( "person", "/person/:uniquename/:action", action="view"
), etc.
Now when rendering pages, I typically have a "Person" object hanging
around (call it "p"), and if I want to link to the correct page I can
of course do something like: url_for( 'person',
uniquename=p.getUniqueName() ) and that works well. The problem is I've
exposed some of the URL generation implementation. Ideally, I'd like to
just pass in the "Person" object, and have the route definition
extended to encapsulalte all of the implementation. I could add a
"generateUrl" method to my Person class to do this, but this binds
controller and iew semantics to my data model. Ideally all URL
management would be in one place, which seems to be one of the points
of Routes.
For example, I'd like to say: url_for( 'person', person = p ), and have
the URL be exactly the same as if I called url_for( 'person',
uniquename=p.getUniqueName() ). Does this make sense and is it a common
pattern?
So it seems to me what's needed is some ability to arbitrarily
transform passed argument values pairs during URL generation. Since I
wrap all of my calls to Routes, I was able to add a pre-processing
filter mechanism, like so:
connect( 'person', route='person/:uniquename/:action',
requirements = dict( action='(view|edit|delete)' ),
filters = [ lambda person: dict( uniquename =
person.getUniqueName() ) ]
... more stuff ... )
the "filters" argument is a list of transformation functions, applied
before calling Routes URL generation, so that I can generate the URL
for 'Person' objects either by url_for( 'person', uniquename= ... ) or
url_for( 'person', person=... ).
Useful? Am I missing something?
Thanks,
-- David
http://www.zachary.com/blog
> Now when rendering pages, I typically have a "Person" object hanging
> around (call it "p"), and if I want to link to the correct page I can
> of course do something like: url_for( 'person',
> uniquename=p.getUniqueName() ) and that works well. The problem is
> I've
> exposed some of the URL generation implementation. Ideally, I'd
> like to
> just pass in the "Person" object, and have the route definition
> extended to encapsulalte all of the implementation. I could add a
> "generateUrl" method to my Person class to do this, but this binds
> controller and iew semantics to my data model. Ideally all URL
> management would be in one place, which seems to be one of the points
> of Routes.
It sounds like at this point, Routes would be doing more than just
making and matching URL's. It's now performing logic on objects to
extract variables before doing route generation?
> For example, I'd like to say: url_for( 'person', person = p ), and
> have
> the URL be exactly the same as if I called url_for( 'person',
> uniquename=p.getUniqueName() ). Does this make sense and is it a
> common
> pattern?
I'm not sure how common it is, I've typically just done what your
original example said.
> connect( 'person', route='person/:uniquename/:action',
> requirements = dict( action='(view|edit|delete)' ),
>
> filters = [ lambda person: dict( uniquename =
> person.getUniqueName() ) ]
>
> the "filters" argument is a list of transformation functions, applied
> before calling Routes URL generation, so that I can generate the URL
> for 'Person' objects either by url_for( 'person', uniquename= ... ) or
> url_for( 'person', person=... ).
>
> Useful? Am I missing something?
Interesting... though I can stare at your example for quite awhile
without really seeing whats going on, so I wonder how much complexity
this would add. It seems like a lot of complexity to add, when just
passing in the uniquename to begin with solves the problem.
Right now, its a fairly consistent rule that route generation
arguments are all strings, to be used to construct the URL. If you
continue to expand on the filter approach, how long before you can't
use url_for without constantly referencing the Routing to see what
object it actually requires? That's my main worry.
Anyone else have any thoughts?
Cheers,
Ben
The specification of filters seems like it is overly generic and
possibly confusing for people. I can't imagine most people are going to
need that much flexibility. How often do you need a list of filters?
Would simply looking for a known method -- _get_routes_unique_id, for
instance -- on a passed in object be sufficient? connect could
conceivably be extended to automatically generate unique ids in some
cases if this function doesn't exist. If type(object) is SQLObject it
could use object.id, for instance.
> though I can stare at your example for quite awhile without really seeing whats going on
I think the example could be simplified somewhat. I don't think
returning a dict is necessary, is it? There's only one element...just
return a single string instead.
filters = [lambda person: person.getUniqueName()]
And if you used a classmethod or regular function it becomes a little
clearer
filters = [Person.getUniqueName]
(parameter.respond_to?(:to_param) ? parameter.to_param :
parameter).to_s
and the default in base model objects is:
alias_method :to_param, :id
so all your objects will cough up their ID if you don't override it.
So if I'm looking at that right, there's a specific message it looks
for to pull the string, otherwise calls the default string
representation. For a Python equivalent, it means Routes could try
calling your objects _to_route_param() method for its value, and fall
back to the __str__ representation.
I'm more in favor of this approach as it keeps the representation of
the object in the object itself, rather than putting various bits of
logic in Routes.
Wouldn't this handle the case where one wants to pass an object in
and have it cough up its own string for use in Routes?
Cheers,
Ben
But in practice for most situations, people will be using models
inheriting the to_param method alias, so all calls for to_param will
call (or send the message actually) "id" unless you override to_param.
So yeah, it would seem a nice solution and keep the knowledge in the
model object rather than mixing that into Routes. People could just
implement a default _to_route_param() in their base objects and only
override when needed. And loosen the coupling as well between the
object and actual route instruction.
The only other thing missing that it looks like David desired, was
the ability to specify what to use for the route string depending on
the keyword being used. So I'd propose that the _route_param() method
be called with the url_for keyword as an argument.
Thus you could have two routes like so:
map.connect('person', 'person/:uniquename/:action')
map.connect('ids', 'person/:person_id/:action')
In the object you plan on passing as a url_for keyword arg:
def _route_param(self, key):
if key == 'uniquename':
return self.getUniqueName()
elif key == 'person_id':
return self.person_id
else:
return self.name
etc. This way you have the option of returning a different value from
the object depending on the keyword its being used as. Purely
optional of course, you could just choose to always return the id of
the object, or what have you.
As Todd mentions, if there is no _route_param method on the object,
it'll assume that doing str(object) will give it the intended value.
How's this look?
Cheers,
Ben
def _route_param(self, key)...
...
in your base model and all your derived models could ignore the whole
issue unless they had special needs, right? Seems straightforward but
I'm still newish enough to python to want to confirm...
Yep, or you could go one step farther and try and pick off the
attribute by the key name if it exists, like:
def _route_param(self, key):
try:
return getattr(self, key)
except:
return self.id
So that passing in the object for the keyword uniquename will have
the object try and use its self.uniquename value first. Anyways, this
looks good to me because it keeps the logic of getting the value of
the object, in the object itself. It also gives you more space for
expressive logic than a lambda statement.
- Ben
When generating the url, if I call something like url_for(
'geo-information', zipcode=zipcode), then the _route_param approach
fails me, as it can only transform a single parameter into another
single parameter. It may be a special case, but the I sometimes need to
transform a single object parameter into multiple URL elements.
As to the point about coupling, that doesn't seem quite right to me.
Routes is essentially controller logic, so it seems that it's fine for
it to know something about the data model. Putting "_route_param" into
my data model object exposes information about the controller to the
model -- that doesn't seem quite right.
-- David
http://www.zachary.com/blog
def my_filter( kwargs_dict ):
....
return modified_kwargs_dict
m.connect( 'person', 'person/:uniquename/:action', action='view', ... ,
_filter=my_filter, ... )
or similar?
I've already implemented this in my application, so I'm happy -- just
curious if this is generally useful.
Ah! I like the looks of that. I'd then assume that the my_filter
should return a keyword dict which is then used to generate the
route, thus the my_filter gets a chance to add arguments, or alter
existing ones. That works for me, I'd be thrilled to add this to Routes.
Just when I think there's nothing more to expand on in Routes... a
good idea like this comes along. :)
Going back to your original example, I'd assume your my_filter for
the 'person' named route might look like:
def my_filter(kwargs):
if kwargs.has_key('person'):
kwargs['uniquename'] = person.getUniqueName()
return kwargs
Cheers,
Ben
>> m.connect( 'person', 'person/:uniquename/:action',
>> action='view', ... ,
>> _filter=my_filter, ... )
> def my_filter(kwargs):
> if kwargs.has_key('person'):
> kwargs['uniquename'] = kwargs['person'].getUniqueName()
> return kwargs
Actually, it'd have to del kwargs['person'] as well, so it wouldn't
be added as a query arg to the route generator. I like the
flexibility and power this add's, using a function like this in
filter makes it a bit more concise and easier to read than the lambda
version as well.
- Ben
On a tangent, I've always been a fan of the following, but more so
regarding visual or industrial design:
A designer knows he has achieved perfection not when there is nothing
left to add, but when there is nothing left to take away. ~ Antoine de
Saint-Exupery
That said, this seems like nice added flexibility. I was playing around
with having the model obj know how to render various paths based on
_route_param() type scenario, but I think I may be getting in over my
head.
Still feels to me that the my_filter type functionality should be in
the model (activerecord-ish bias I guess), like the model knowing how
to respond to "uniquename" but I've no alternative implementation at
this point to offer, so my argument doesn't carry much weight here!
That seems like it would be a bit more explicit somehow.
Maybe the keyword could be something other than '_filter' ? Something
along the lines of (_alter, _post_process, _represent, _manipulate)?
It seems as if it's sort of being used as a general-purpose rewriter,
no? I mean you're basically turning ":uniquename" into a wildcard or
placeholder for an arbitrary dict, right? 'filter' is one of those
catch-all terms, so I guess it sorta works but isn't esp. clear.
I'm just trying to keep things clear and transparent for the newcomers
(myself)... and while I see a use for this functionality, it seems to
be slipping a little bit into that "makes sense to me" territory
instead of "obvious to everyone". So view this as my .02.
Well you're certainly singing my song with that quote (I'm a big fan of
Saint-Exupery). To that end, adding this capability has allowed me to
remove a fair amount of brittle code from the rest of my app. When
marketing says "put the person's name in the URL instead of an ID,"
none of the templates change. Seems like a good thing -- this sort of
encapsulation is exactly what I like about Routes.
-- David
http://www.zachary.com/blog
> That said, this seems like nice added flexibility. I was playing
> around
> with having the model obj know how to render various paths based on
> _route_param() type scenario, but I think I may be getting in over my
> head.
The _route_param is useful to pull attributes out of an object for
use in a Route, however it can't actually add keys to the route being
generated. I think a _route_param is still useful in addition to a
way to have a function process the keyword args and update/modify
them before actually generating the URL.
> Still feels to me that the my_filter type functionality should be in
> the model (activerecord-ish bias I guess), like the model knowing how
> to respond to "uniquename" but I've no alternative implementation at
> this point to offer, so my argument doesn't carry much weight here!
> That seems like it would be a bit more explicit somehow.
It would be explicit if it was merely giving a different string
representation, the filter type thing goes beyond that.
> Maybe the keyword could be something other than '_filter' ? Something
> along the lines of (_alter, _post_process, _represent, _manipulate)?
> It seems as if it's sort of being used as a general-purpose rewriter,
> no? I mean you're basically turning ":uniquename" into a wildcard or
> placeholder for an arbitrary dict, right? 'filter' is one of those
> catch-all terms, so I guess it sorta works but isn't esp. clear.
>
> I'm just trying to keep things clear and transparent for the newcomers
> (myself)... and while I see a use for this functionality, it seems to
> be slipping a little bit into that "makes sense to me" territory
> instead of "obvious to everyone". So view this as my .02.
I think _route_param would be something a lot of people would use,
while the _filter thing is more advanced functionality that can be
explained in a separate section since many people aren't going to
need it.
In case it isn't clear, and to ensure I'm understanding it right,
I'll sum up the proposal:
When generating a route, it can be useful, especially if multiple
dynamic paths from the Route are all in a single object, to pass
*just* the object in. For example, consider this route:
map.connect('archive', 'archives/:year/:month/:day/:slug',
controller='archives', action='view')
To generate a route that gives you this URL, even though its named to
save you some time, you will need 4 keyword arguments, like so:
url_for('archive', year=2004, month=10, day=3, slug='yea_for_routes')
Typically if you know all this information, its quite likely that you
have an object which contains it all, whether an ORM class, etc.
Perhaps its a SQLObject class so using it would look like this (with
a story object):
url_for('archive', year=story.date.year, month=story.date.month,
day=story.date.day, slug=story.date.slug)
Wow! What a pain. :)
Let's add in a filter:
def article_filter(kargs):
story = kargs.get('article')
if story:
del kargs['article']
kargs.update(
dict(year=story.date.year, month=story.date.month,
day=story.date.day, slug=story.date.slug)
)
return kargs
map.connect('archive', 'archives/:year/:month/:day/:slug',
controller='archives', action='view', _filter=article_filter)
Now anytime we need to make a URL, we just pass in a keyword called
'article', that we know our article_filter will take care of like so:
url_for('archive', article=story)
Before generating the URL, Routes will take the keyword arguments,
give it to article filter, then use the resulting dict as the actual
arguments.
I'd consider this a pretty powerful feature, and something like this
definitely belongs with the Routes, vs the _route_param which is
merely expanding an object into a string, and not altering the
keywords used for the route.
As I mentioned though, this will be in a separate "Advanced Usage"
section of Routes, as many people, especially at the beginning, won't
need to use it.
Is filter a good name? Would _process_keys or something be a little
more obvious?
Cheers,
Ben
Yes, that's it exactly -- thanks Ben for summarizing what I found
difficult to say. BTW, dict.pop is a nice shortcut for the get / del
pattern:
def article_filter(kargs):
story = kargs.pop('article', None)
if story:
kargs.update(
dict(year=story.date.year, month=story.date.month,
day=story.date.day, slug=story.date.slug)
)
return kargs
Looking forward to Routes 1.3 :-)
-- David
http://www.zachary.com/blog
> Looking forward to Routes 1.3 :-)
This feature is now in the Routes dev for those interested in giving
it a whirl,
easy_install -U Routes==dev
Changeset with test:
http://routes.groovie.org/trac/routes/changeset/182
Please note that the _filter must be used with a named route, if the
Route has no name there'd be no way to know which filter to use. :)
I would like a more obvious name than _filter, any suggestions?
To clarify, the word should represent an application of a function to
set of arguments that returns a possibly new set of arguments.
Cheers,
Ben
> Still feels to me that the my_filter type functionality should be in
> the model (activerecord-ish bias I guess), like the model knowing how
> to respond to "uniquename" but I've no alternative implementation at
> this point to offer, so my argument doesn't carry much weight here!
It's a bit tricky I think in this case. On the one hand for this
method needs to be both aware that its used for Routing, and that its
dealing with a model. So I'm not sure there is any "right" place to
put it since it deals with fairly intimate knowledge of both sides.
I'm inclined now to think the _filter function belongs with the
Routes just so you can have a single place to go to figure out whats
being pulled out of what to make the route.
Cheers,
Ben
route_generator?
route_mapper?
Hmm...I think I like route_mapper better than filter.
Naming... the most obvious seems 'mapper' but in this context (being
passed to a Mapper) maybe confusing.
Just to throw some out there: render, convert, obj_mapper (or
route_mapper as Justus mentioned), translate, represent, expand, or....
filter. Thingamajig?
This is an excellent addition, very powerful.
>
> I would like a more obvious name than _filter, any suggestions?
>
Since it's being passed to mapper.connect, how about connector?
Other alternatives - transformer, router, remapper, mutator, adaptor, switch.
> To clarify, the word should represent an application of a function to
> set of arguments that returns a possibly new set of arguments.
>
Hmm, I think transformer fits this description pretty well, but I like the
mental image that connector brings with it in the context of a mapper.
Robert