I hate to request more changes to magic-removal... but that's where this should happen. Maybe it should wait until after it's been merged though.
Currently the redirects after add_stage and change_stage in the admin system are hardcoded. One of the most frequent requests I get in my projects is to allow people to go to some other related object after they save, not back to the list of objects. The redirect should be customizable. Here's how I propose it should be done.
Add a template block {% block submitrow %} around the submit_row in the admin/change_form.html. This would allow people to override the save buttons. It would be cool to do this with a combination of template tags, and attributes in the inner Admin class, but a block would still be useful to have. If anyone has suggestions on how attributes+template tags might work, I'd like to hear them. I haven't thought about it too much yet.
By default, the inner Admin class should have a couple of new methods (or attributes that are assigned to a callable) One callable returns an HttpResponseRedirect for adding, and the other, after changing. These callables would take the request and the new (or updated) object as arguments. So the default for change_stage would look like:
def after_change_action(request, new_object): pk_value = getattr(new_object, opts.pk.attname) if request.POST.has_key("_continue"): request.user.add_message(msg + ' ' + _("You may edit it again below.")) if request.REQUEST.has_key('_popup'): return HttpResponseRedirect(request.path + "?_popup=1") else: return HttpResponseRedirect(request.path) elif request.POST.has_key("_saveasnew"): request.user.add_message(_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': opts.verbose_name, 'obj': new_object}) return HttpResponseRedirect("../../%s/" % pk_value) elif request.POST.has_key("_addanother"): request.user.add_message(msg + ' ' + (_("You may add another %s below.") % opts.verbose_name)) return HttpResponseRedirect("../../add/") else: request.user.add_message(msg) return HttpResponseRedirect("../../")
Another option would be to have the callable return a (url, message) tuple, and let the view handle HttpResponseRedirect and request.user.add_message. I think it depends on whether people think it would be useful to do anything but a redirect on successful adding/editing of an object using the admin system.
It would be nice to be able to override log_change_message in a similar way as well, but I think rjwittams has something event based in mind for that. I could be wrong.
At any rate, I think the new inner Admin class is a clean place to put this kind of stuff.
I think this is a very good idea, and I like the precedent of adding methods to the inner Admin class to influence the admin behavior. I do agree, however, that this should wait until after we finish the magic-removal branch. There's nothing in this proposed change that would break any existing code, so let's keep the ball moving and take care of this slightly later.
> Another option would be to have the callable return a (url, message)
tuple, and let the view handle HttpResponseRedirect and request.user.add_message.
I think you should use a dictionary. For example, you could pass in the following dictionary: {None:'../report1/%s'} and then in the default method: else: request.user.add_message(msg) redir = redir_dict.get(None, '../../') return HttpResponseRedirect(redir)
etc. for all types of POST in the method.
Anyway, you could pass this into the change_stage method via the admin class, or via urls.py (which is how I'm using this technique). ('url1/', 'change_list', {'app_label':'myapp', 'module_name':'mymod'}), ('url1/\d+/', 'change_stage', {'app_label':'myapp', 'module_name': 'mymod', 'redir_dict':{None:'../report1/%s'}}), ('url1/add/', 'add_stage', {'app_label':'myapp','module_name':'mymod','redir_dict':{None:'../report1/% s'}}),
or you could write a tiny custom view that does the same and looks cleaner!
Of course you could throw the same dict into the admin class to achieve the same.
The reason I think it is nice to do this without a custom "after_change_action" method is that much of the after_change_action code would be duplicated on different modules, but the actual url is the part that changes. Also much of the time you will just want to redir the "Save" action and none of the others.
> > Another option would be to have the callable return a (url, message) > tuple, and let the view handle HttpResponseRedirect and > request.user.add_message.
> I think you should use a dictionary. For example, you could pass in the > following dictionary: > {None:'../report1/%s'} > and then in the default method: > else: > request.user.add_message(msg) > redir = redir_dict.get(None, '../../') > return HttpResponseRedirect(redir)
> etc. for all types of POST in the method.
> Anyway, you could pass this into the change_stage method via the admin > class, or via urls.py (which is how I'm using this technique). > ('url1/', 'change_list', {'app_label':'myapp', 'module_name':'mymod'}), > ('url1/\d+/', 'change_stage', {'app_label':'myapp', 'module_name': > 'mymod', 'redir_dict':{None:'../report1/%s'}}), > ('url1/add/', 'add_stage', > {'app_label':'myapp','module_name':'mymod','redir_dict':{None:'../report1/% s'}}),
> or you could write a tiny custom view that does the same and looks > cleaner!
> Of course you could throw the same dict into the admin class to achieve > the same.
> The reason I think it is nice to do this without a custom > "after_change_action" method is that much of the after_change_action > code would be duplicated on different modules, but the actual url is > the part that changes. Also much of the time you will just want to > redir the "Save" action and none of the others.
> -rob
I've been looking at the same thing in the last day and I found the 'post_url' keyword arg to the add_stage() view that allows you to redirect somewhere after adding a new object but this arg doesn't exist for the change_stage() view. Would adding a similar argument to change_stage() solve this?
On 1/11/06, Matthew Flanagan <mattimust...@gmail.com> wrote:
> I've been looking at the same thing in the last day and I found the > 'post_url' keyword arg to the add_stage() view that allows you to > redirect somewhere after adding a new object but this arg doesn't > exist for the change_stage() view. Would adding a similar argument to > change_stage() solve this?
Kind of, but that wouldn't allow you to specify new save button action types, ("save and continue editing", "save and add X", etc.) and you'd have to override urls to get the custom redirect. I'd rather avoid that if possible.
Also, I don't believe post_url would allow you to use something like the new object id in your url.
On 1/12/06, Joseph Kocherhans <jkocherh...@gmail.com> wrote:
> On 1/11/06, Matthew Flanagan <mattimust...@gmail.com> wrote:
> > I've been looking at the same thing in the last day and I found the > > 'post_url' keyword arg to the add_stage() view that allows you to > > redirect somewhere after adding a new object but this arg doesn't > > exist for the change_stage() view. Would adding a similar argument to > > change_stage() solve this?
> Kind of, but that wouldn't allow you to specify new save button action > types, ("save and continue editing", "save and add X", etc.) and you'd > have to override urls to get the custom redirect. I'd rather avoid > that if possible.
Similar to a "wizard" style interface? This would be handy for complex models.
> Also, I don't believe post_url would allow you to use something like > the new object id in your url.
> Also, I don't believe post_url would allow you to use something like
the new object id in your url.
I needed this capability (I have wizard-like interfaces derived from the admin pages), and ended up modifying change_stage so it read something like this: redir = redir_dict.get(None, '../../') # actually I just used a post_url arg like in add_stage if redir.find('%s') >= 0: redir = redir % pk_value return httpwrappers.HttpResponseRedirect(redir)
and I pass in post_url strings like '/report1/%s/'
Still this only works in simple situations (e.g. you cannot derive other info from the POST like perhaps a date field as well, to format a url like '/report1/123/2006/01/') so I think there is still value in custom after_change_action methods.
On 1/11/06, Joseph Kocherhans <jkocherh...@gmail.com> wrote:
> I hate to request more changes to magic-removal... but that's where > this should happen. Maybe it should wait until after it's been merged > though.
> Currently the redirects after add_stage and change_stage in the admin > system are hardcoded. One of the most frequent requests I get in my > projects is to allow people to go to some other related object after > they save, not back to the list of objects. The redirect should be > customizable. Here's how I propose it should be done.
> Add a template block {% block submitrow %} around the submit_row in > the admin/change_form.html. This would allow people to override the > save buttons.
This just occured to me today. I think it's flexible enough without being too complicated.
def continue(request, new_object, msg): "The save and continue action for the admin system." pk_value = new_object._get_pk_val() request.user.add_message(msg + ' ' + _("You may edit it again below.")) if request.POST.has_key("_popup"): post_url_continue += "?_popup=1" return HttpResponseRedirect(post_url_continue % pk_value)
default_add_options = ( ('Save and Continue Editing', '_continue', continue), ('Save and Add Another', '_addanother', add_another), ('Save', '_save', save), )
class Article(models.Model): title = models.CharField(maxlength=100)
Admin: # use the defaults, but nix the first option # the default options should be easy to import so people can modify them add_options = default_add_options[1:]
# use a totally custom set of options change_options = ( ('Save and Continue Editing', '_continue', continue), ('Save', '_save', save), ('Save and Add Another', '_addanother', add_another), )
add_options and change_options are tuples of tuples. Each (VERBOSE_NAME, POST_VAR_NAME, CALLABLE) tuple corresponds to a button/action. Now, when there are no validation errors, the admin does something like this:
new_object = manipulator.save(new_data) # admin log stuff msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': opts.verbose_name, 'obj': new_object} for save_option in opts.change_options: if request.POST.has_key(save_option[1]): return save_option[2](request, new_object, msg)
Obviously, the first matching one gets called even if more are present in the post data. This might trip users up, but I think checking for more than one in the post data is overkill. Maybe the model validator should check though *shrug*
There are a few different options for deciding which is the default. The options should be displayed in order, so.
1. The last option could implicitly be the default. 2. Add one more item, 'default', at the the end of the default options. (yuck!) 3. Another couple of Admin attributes set to the HTTP var name: default_add_option = '_continue' default_change_option = '_addanother'
I like a combination of 1 and 3 the best. Implicitly use the last tuple as the default (so there's ALWAYS a default) and override that default with default_add/change_option.
Thoughts?
Also, the post_url='../', post_url_continue='../%s/' args should be removed from add_stage, or added to change_stage. My vote is for removing them, or maybe we could pass in a tuple of options exactly like the new Admin attributes instead. If custom options get passed in, use them instead of the ones from the model, or maybe try to merge them with the passed ones taking precedence.
As far as I can see, the form_url='' argument for add_stage and render_change_form aren't used correctly either. (the add_stage's form_url arg never gets passed in to render_change_form) Are these still needed?