fu: Close-up of Fu, bringing a scoop of water to her mouth (Default)
fu ([personal profile] fu) wrote in [site community profile] changelog2010-10-28 03:06 am

[dw-free] Convert /admin/index to TT

[commit: http://hg.dwscoalition.org/dw-free/rev/61913b5952d1]

http://bugs.dwscoalition.org/show_bug.cgi?id=3164

Use controllers/TT for the admin page. Admin pages written in the new style
can register themselves rather than having to have us manually list them.

Patch by [personal profile] exor674.

Files modified:
  • cgi-bin/DW/Controller/Admin.pm
  • cgi-bin/DW/Controller/Rename.pm
  • cgi-bin/DW/Controller/SiteStats.pm
  • cgi-bin/DW/Routing.pm
  • cgi-bin/DW/Template/Plugin.pm
  • cgi-bin/DW/Template/VMethods.pm
  • htdocs/admin/index.bml
  • htdocs/admin/index.bml.text
  • t/routing.t
  • views/admin/index.tt
  • views/admin/index.tt.text
  • views/admin/rename.tt
  • views/admin/rename.tt.text
  • views/admin/stats.tt.text
--------------------------------------------------------------------------------
diff -r f4fcc06b2940 -r 61913b5952d1 cgi-bin/DW/Controller/Admin.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Controller/Admin.pm	Thu Oct 28 11:06:03 2010 +0800
@@ -0,0 +1,319 @@
+#!/usr/bin/perl
+#
+# DW::Controller::Admin
+#
+# Controller for admin action list index pages.
+#
+# Authors:
+#      Andrea Nall <anall@andreanall.com>
+#      Denise Paolucci <denise@dreamwidth.org>
+#      Sophie Hamilton <dw-bugzilla@theblob.org>
+#
+# Copyright (c) 2009-2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package DW::Controller::Admin;
+
+=head1 NAME
+
+DW::Controller::Admin - Controller for admin action list index pages.
+
+This is for pages like /admin/index which list other pages, displaying only what the current user can do
+
+=head1 API
+
+=cut
+
+use strict;
+use warnings;
+use DW::Controller;
+use DW::Routing;
+use DW::Template;
+
+my $admin_pages = {};
+
+DW::Controller::Admin->register_admin_scope( '/', title_ml => '.admin.title' );
+
+# DO NOT add anything to here
+DW::Controller::Admin->_register_admin_pages_legacy( '/', 
+    [ 'capedit', '.admin.capability.link', '.admin.capability.text', [ 'admin:*', sub {
+        return ( $LJ::IS_DEV_SERVER, LJ::Lang::ml( "/admin/index.tt.devserver" ) );
+    } ] ],
+    [ 'clusterstatus',
+        '.admin.cluster.link', '.admin.cluster.text', [ 'supporthelp' ] ],
+    [ 'console/',
+        '.admin.console.link', '.admin.console.text' ],
+    [ 'schema/',
+        '.admin.dbschema.link', '.admin.dbschema.text', [ 'schemadoc' ] ],
+    [ 'dupkiller',
+        '.admin.dupkiller.link', '.admin.dupkiller.text', [ 'supporthelp' ] ],
+    [ 'entryprops',
+        '.admin.entryprops.link', '.admin.entryprops.text', [ 'canview:entryprops', 'canview:*', sub {
+            return ( $LJ::IS_DEV_SERVER, LJ::Lang::ml( "/admin/index.tt.devserver" ) );
+    } ] ],
+    [ 'faq/',
+        '.admin.faq.link', '.admin.faq.text', [ 'faqadd', 'faqedit', 'faqcat' ] ],
+    [ 'fileedit/',
+        '.admin.file_edit.link', '.admin.file_edit.text', [ 'fileedit' ] ],
+    [ 'invites/', '.admin.invites.link', '.admin.invites.text', [ 'finduser:codetrace', 'finduser:*', 'payments' ] ],
+    [ 'logout_user',
+        '.admin.logout_user.link', '.admin.logout_user.text', [ 'suspend' ] ],
+    [ 'memcache',
+        '.admin.memcache.link', '.admin.memcache.text', [ 'siteadmin:memcacheview', 'siteadmin:*' ] ],
+    [ 'memcache_view',
+        '.admin.memcache_view.link', '.admin.memcache_view.text', [ 'siteadmin:memcacheview', 'siteadmin:*', sub {
+            return ( $LJ::IS_DEV_SERVER, LJ::Lang::ml( "/admin/index.tt.devserver" ) );
+        } ] ],
+    [ 'mysql_status',
+        '.admin.mysql.link', '.admin.mysql.text', [ 'siteadmin:mysqlstatus', 'siteadmin:*' ] ],
+    [ 'navtag',
+        '.admin.navtag.link', '.admin.navtag.text', [ 'siteadmin:navtag', 'siteadmin:*' ] ],
+    [ 'pay/',
+        '.admin.pay.link', '.admin.pay.text', [ 'payments' ] ],
+    [ 'priv/',
+        '.admin.priv.link', '.admin.priv.text' ],
+    [ 'propedit',
+        '.admin.propedit.link', '.admin.propedit.text', [ 'canview:userprops', 'canview:*' ] ],
+    [ 'recent_comments',
+        '.admin.recent_comments.link', '.admin.recent_comments.text', [ 'siteadmin:commentview', 'siteadmin:*' ] ],
+    [ 'sitemessages/add',
+        '.admin.sitemessages_add.link', '.admin.sitemessages_add.text', [ 'siteadmin:sitemessages', 'siteadmin:*', sub {
+            return ( $LJ::IS_DEV_SERVER, LJ::Lang::ml( "/admin/index.tt.devserver" ) );
+        } ] ],
+    [ 'sitemessages/manage',
+        '.admin.sitemessages_manage.link', '.admin.sitemessages_manage.text', [ 'siteadmin:sitemessages', 'siteadmin:*', sub {
+            return ( $LJ::IS_DEV_SERVER, LJ::Lang::ml( "/admin/index.tt.devserver" ) );
+        } ] ],
+    [ 'spamreports',
+        '.admin.spamreports.link', '.admin.spamreports.text', [ 'siteadmin:spamreports', 'siteadmin:*' ] ],
+    [ 'statushistory',
+        '.admin.statushistory.link', '.admin.statushistory.text', [ 'historyview', sub {
+            return ( $LJ::IS_DEV_SERVER, LJ::Lang::ml( "/admin/index.tt.devserver" ) );
+        } ] ],
+    [ 'styleinfo',
+        '.admin.styleinfo.link', '.admin.styleinfo.text', [ sub {
+            return ( LJ::Support::has_any_support_priv($_[0]->{remote}),
+                LJ::Lang::ml( "/admin/index.tt.anysupportpriv" ) );
+        }, sub {
+            return ( $LJ::IS_DEV_SERVER, LJ::Lang::ml( "/admin/index.tt.devserver" ) );
+        } ] ],
+    [ 'sysban',
+        '.admin.sysban.link', '.admin.sysban.text', [ 'sysban' ] ],
+    [ 'theschwartz',
+        '.admin.theschwartz.link', '.admin.theschwartz.text', [ 'siteadmin:theschwartz' ] ],
+    [ 'translate/',
+        '.admin.translate.link', '.admin.translate.text' ],
+    [ 'userlog',
+        '.admin.userlog.link', '.admin.userlog.text', [ 'canview:userlog', 'canview:*' ] ],
+);
+
+
+sub admin_handler {
+    my $opts = shift @_;
+    my $r = DW::Request->get;
+
+    my ( $ok, $rv ) = controller();
+    return $rv unless $ok;
+
+    my $remote = $rv->{remote};
+
+    my $args = $opts->args || {};
+    my $scope = $args->{scope} || "/";
+
+    my $data = $admin_pages->{$scope};
+
+    my $vars = $rv;
+
+    $vars->{$_} = $data->{$_} foreach qw( title_ml description_ml ml_scope );
+
+    my @pages;
+    my $adminstar = $remote && $remote->has_priv( 'admin', '*' );
+    foreach my $page ( @{ $data->{pages} } ) {
+        my ( $path, $link_ml, $description_ml, $privs ) = @{$page};
+        my $showpage = 0;
+        my ( @needsprivs, @gotprivs );
+        my $haspriv = 0;
+        foreach my $priv ( @{$privs} ) {
+            my $code_result;
+            $code_result = [
+                $priv->( { remote => $remote } )
+            ] if ref( $priv ) eq "CODE";
+
+            my $result = ( $code_result ?
+                             $code_result->[0] :
+                             $remote && $remote->has_priv( split( /:/, $priv ) ) );
+            my $displayedpriv = ( $code_result ?
+                                        $code_result->[1] :
+                                        $priv );
+            push( @gotprivs,   $displayedpriv ) if $result;
+            push( @needsprivs, $displayedpriv ) if !$result;
+            $haspriv  = 1 if $result;
+            $showpage = 1 if $adminstar || $result;
+        }
+        if ( @$privs == 0 ) {
+            $showpage = 1;
+            $haspriv  = 1;
+        }
+        $path = "/admin$scope$path" unless $path =~ m!^((https?://)|/)!;
+        if ( $showpage ) {
+            push @pages, {
+                path => $path,
+                link_ml => $link_ml,
+                description_ml => $description_ml,
+                haspriv => $haspriv,
+                gotprivs => \@gotprivs,
+                needsprivs => \@needsprivs,
+            };
+        }
+    }
+    $vars->{pages} = \@pages;
+
+    return DW::Template->render_template( 'admin/index.tt', $vars );
+}
+
+=head2 C<< $class->register_regex( $scope, %opts ) >>
+
+Register an admin scope.
+
+Arguments:
+
+=over 4
+
+=item scope
+
+The name of the scope
+
+=back
+
+Additional options:
+
+=over 4
+
+=item ml_scope
+
+The ML scope for the ml strings.
+
+=item title_ml
+
+ML string for title
+
+=item description_ml
+
+ML string for description
+
+=back
+
+=cut
+
+sub register_admin_scope {
+    my ( $class, $scope, %opts ) = @_;
+
+    $admin_pages->{$scope} = \%opts;
+
+    my $url = "/admin" . ( $scope eq '/' ? '' : $scope ) . '/index';
+
+    DW::Routing->register_string( $url, \&admin_handler, args => { scope => $scope } );
+}
+
+=head2 C<< $class->register_admin_page( $scope, %opts ) >>
+
+Register an admin scope.
+
+Arguments:
+
+=over 4
+
+=item scope
+
+The name of the scope
+
+=back
+
+Additional options:
+
+=over 4
+
+=item ml_scope
+
+The ML scope for the ml strings.
+
+=item link_ml
+
+ML string for link text ( defaults to ".admin.link" )
+
+=item description_ml
+
+ML string for description ( defaults to ".admin.text" )
+
+=item path
+
+Path, either relative to the scope ( no leading slash ), relative to the domain ( leading slash ), or a full URI
+
+=item privs
+
+An arrayref of priv names or subroutine refs
+
+The subref needs to return [ $has_priv, $string ]
+
+=back
+
+=cut
+
+sub register_admin_page {
+    my ( $class, $scope, %args ) = @_;
+
+    my ( $path, $link_ml, $desc_ml, $privs );
+
+    $path = $args{path};
+    $privs = $args{privs};
+
+    $link_ml = $args{link_ml} || ".admin.link";
+    $desc_ml = $args{description_ml} || ".admin.text";
+
+    if ( $args{ml_scope} ) {
+        $link_ml = $args{ml_scope} . $link_ml;
+        $desc_ml = $args{ml_scope} . $desc_ml;
+    } 
+
+    $scope ||= "/";
+    push @{$admin_pages->{$scope}->{pages}}, [ $path, $link_ml, $desc_ml, $privs ];
+}
+
+# DO NOT USE OUTSIDE THIS FILE!
+# FIXME: Remove once the big scary array up above is gone!
+sub _register_admin_pages_legacy {
+    my ( $class, $scope, @pages ) = @_;
+
+    $scope ||= "/";
+    foreach my $part ( @pages ) {
+        push @{$admin_pages->{$scope}->{pages}}, $part;
+    }
+}
+
+=head1 AUTHOR
+
+=over
+
+=item Andrea Nall <anall@andreanall.com>
+
+=item Denise Paolucci <denise@dreamwidth.org>
+
+=item Sophie Hamilton <dw-bugzilla@theblob.org>
+
+=back
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2009-2010 by Dreamwidth Studios, LLC.
+
+This program is free software; you may redistribute it and/or modify it under
+the same terms as Perl itself. For a copy of the license, please reference
+'perldoc perlartistic' or 'perldoc perlgpl'.
+
+=cut
+
+1;
diff -r f4fcc06b2940 -r 61913b5952d1 cgi-bin/DW/Controller/Rename.pm
--- a/cgi-bin/DW/Controller/Rename.pm	Thu Oct 28 10:15:22 2010 +0800
+++ b/cgi-bin/DW/Controller/Rename.pm	Thu Oct 28 11:06:03 2010 +0800
@@ -21,6 +21,7 @@ use DW::Controller;
 use DW::Controller;
 use DW::Routing;
 use DW::Template;
+use DW::Controller::Admin;
 
 use DW::RenameToken;
 use DW::Shop;
@@ -30,10 +31,16 @@ use DW::Shop;
 # ideally, should be: /rename, or /rename/(20 character token)
 DW::Routing->register_regex( qr!^/rename(?:/([A-Z0-9]*))?$!i, \&rename_handler, app => 1 );
 
-DW::Routing->register_string( "/admin/rename", \&rename_admin_handler, app => 1 );
+DW::Routing->register_string( "/admin/rename/index", \&rename_admin_handler, app => 1 );
 DW::Routing->register_string( "/admin/rename/edit", \&rename_admin_edit_handler, app => 1 );
 
 DW::Routing->register_string( "/admin/rename/new", \&siteadmin_rename_handler, app => 1 );
+
+DW::Controller::Admin->register_admin_page( '/',
+    path => '/admin/rename/',
+    ml_scope => '/admin/rename.tt',
+    privs => [ 'siteadmin:rename' ]
+);
 
 sub rename_handler {
     my $r = DW::Request->get;
diff -r f4fcc06b2940 -r 61913b5952d1 cgi-bin/DW/Controller/SiteStats.pm
--- a/cgi-bin/DW/Controller/SiteStats.pm	Thu Oct 28 10:15:22 2010 +0800
+++ b/cgi-bin/DW/Controller/SiteStats.pm	Thu Oct 28 11:06:03 2010 +0800
@@ -34,6 +34,7 @@ use DW::Template;
 use DW::Template;
 use DW::StatStore;
 use DW::StatData;
+use DW::Controller::Admin;
 
 LJ::ModuleLoader::autouse_subclasses( 'DW::StatData' );
 
@@ -43,7 +44,11 @@ DW::Routing->register_string( '/admin/st
 DW::Routing->register_string( '/admin/stats', \&stats_page, app => 1,
                               args => [ 'admin/stats.tt', \&admin_data, 0,
                                         'payments' ] );
-
+DW::Controller::Admin->register_admin_page( '/',
+    path => '/admin/stats',
+    ml_scope => '/admin/stats.tt',
+    privs => [ 'payments' ]
+);
 =head1 Internals
 
 =head2 C<< DW::Controller::SiteStats::stats_page( $opts ) >>
diff -r f4fcc06b2940 -r 61913b5952d1 cgi-bin/DW/Routing.pm
--- a/cgi-bin/DW/Routing.pm	Thu Oct 28 10:15:22 2010 +0800
+++ b/cgi-bin/DW/Routing.pm	Thu Oct 28 11:06:03 2010 +0800
@@ -107,13 +107,16 @@ sub get_call_opts {
     # us accessors.
     my $call_opts = DW::Routing::CallInfo->new( \%opts );
 
+    my $hash;
+    my $role = $call_opts->role;
+
     # try the string options first as they're fast
-    my $hash = $string_choices{$call_opts->role . $uri};
+    $hash = $string_choices{$role . $uri};
     if ( defined $hash ) {
         $call_opts->init_call_opts( $hash );
         return $call_opts;
     }
-    
+
     # try the regex choices next
     # FIXME: this should be a dynamically sorting array so the most used items float to the top
     # for now it doesn't matter so much but eventually when everything is in the routing table
@@ -135,7 +138,6 @@ Calls the raw hash.
 Calls the raw hash.
 
 =cut
-
 sub call_hash {
     my ( $class, $opts ) = @_;
     my $r = DW::Request->get;
@@ -147,12 +149,8 @@ sub call_hash {
     return $r->call_response_handler( \&_call_hash );
 }
 
-=head2 C<< $class->_call_hash() >>
-
-Perl Response Handler for call_hash
-
-=cut
-
+# INTERNAL METHOD: no POD
+# Perl Response Handler for call_hash
 sub _call_hash {
     my $r = DW::Request->get;
     my $opts = $r->pnote('routing_opts');
@@ -190,7 +188,6 @@ sub _call_hash {
         $r->status( 500 );
         $r->print(objToJson( { error => $text } ));
         return $r->OK;
-
     # default error rendering
     } else {
         $msg = $err->as_html;
@@ -207,14 +204,23 @@ sub _call_hash {
     }
 }
 
+# INTERNAL METHOD: no POD
+# controller sub for register_static
 sub _static_helper {
     my $r = DW::Request->get;
-    return  DW::Template->render_template( $_[0]->args );
+    return DW::Template->render_template( $_[0]->arg );
+}
+
+# INTERNAL METHOD: no POD
+# controller sub for register_redirect
+sub _redirect_helper {
+    my $r = DW::Request->get;
+    return $r->redirect( $_[0]->args );
 }
 
 =head1 Registration API
 
-=head2 C<< $class->register_static($string, $filename, $opts) >>
+=head2 C<< $class->register_static( $string, $filename, %opts ) >>
 
 Static page helper.
 
@@ -237,7 +243,7 @@ sub register_static {
     $class->register_string( $string, \&_static_helper, %opts );
 }
 
-=head2 C<< $class->register_string($string, $sub, $opts) >>
+=head2 C<< $class->register_string( $string, $sub, %opts ) >>
 
 =over
 
@@ -276,9 +282,47 @@ sub register_string {
     $string_choices{'app'  . $string} = $hash if $hash->{app};
     $string_choices{'ssl'  . $string} = $hash if $hash->{ssl};
     $string_choices{'user' . $string} = $hash if $hash->{user};
+
+    if ( $string =~ m!(^(.+)/)index$! && ! exists $opts{no_redirects} ) {
+        my %opts = (
+            app => $hash->{app},
+            ssl => $hash->{ssl},
+            user => $hash->{user},
+            formats => $hash->{formats},
+            format => $hash->{format},
+            no_redirects => 1,
+        );
+        $class->register_redirect( $2, $1, %opts );
+        $string_choices{'app'  . $1} = $hash if $hash->{app};
+        $string_choices{'ssl'  . $1} = $hash if $hash->{ssl};
+        $string_choices{'user' . $1} = $hash if $hash->{user};
+    }
 }
 
-=head2 C<< $class->register_regex($regex, $sub, $opts) >>
+=head2 C<< $class->register_redirect( $string, $dest, %opts ) >>
+
+Redirect helper.
+
+=over
+
+=item string - path
+
+=item dest - destination
+
+=item Opts ( see register_string )
+
+=back
+
+=cut
+
+sub register_redirect {
+    my ( $class, $string, $dest, %opts ) = @_;
+
+    $opts{args} = $dest;
+    $class->register_string( $string, \&_redirect_helper, %opts );
+}
+
+=head2 C<< $class->register_regex( $regex, $sub, %opts ) >>
 
 =over
 
@@ -306,7 +350,6 @@ sub register_regex {
 
 # internal method, intentionally no POD
 # applies default for opts and hash
-
 sub _apply_defaults {
     my ( $opts, $hash ) = @_;
 
diff -r f4fcc06b2940 -r 61913b5952d1 cgi-bin/DW/Template/Plugin.pm
--- a/cgi-bin/DW/Template/Plugin.pm	Thu Oct 28 10:15:22 2010 +0800
+++ b/cgi-bin/DW/Template/Plugin.pm	Thu Oct 28 11:06:03 2010 +0800
@@ -18,6 +18,7 @@ use strict;
 use strict;
 
 use DW::Template::Filters;
+use DW::Template::VMethods;
 
 =head1 NAME
 
@@ -104,6 +105,26 @@ sub form_auth {
     return LJ::form_auth();
 }
 
+=head2 sort_by_key
+
+Sorts an array of hashrefs by given key
+
+=cut
+
+sub sort_by_key {
+    my $k = $_[2];
+    my $md = $_[3] || 'alpha';
+
+    my @r;
+    if ( $md eq 'alpha' ) {
+        @r = sort { $a->{$k} cmp $b->{$k} } @{$_[1]};
+    } else {
+        @r = sort { $a->{$k} <=> $b->{$k} } @{$_[1]};
+    }
+
+    return \@r;
+}
+
 =head1 AUTHOR
 
 =over
diff -r f4fcc06b2940 -r 61913b5952d1 cgi-bin/DW/Template/VMethods.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Template/VMethods.pm	Thu Oct 28 11:06:03 2010 +0800
@@ -0,0 +1,35 @@
+#!/usr/bin/perl
+#
+# DW::Template::VMethods
+#
+# VMethods for the Dreamwidth Template Toolkit plugin
+#
+# Authors:
+#      Andrea Nall <anall@andreanall.com>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package DW::Template::VMethods;
+use strict;
+use Template::Stash;
+
+$Template::Stash::LIST_OPS->{ sort_by_key } = sub {
+    my ( $lst, $k, $type ) = @_;
+
+    my @r = ();
+    $type ||= 'alpha';
+    if ( $type eq 'alpha' ) {
+        @r = sort { $a->{$k} cmp $b->{$k} } @$lst; 
+    } elsif ( $type eq 'numeric' ) {
+        @r = sort { $a->{$k} <=> $b->{$k} } @$lst; 
+    }
+
+    return \@r;
+};
+
+1;
diff -r f4fcc06b2940 -r 61913b5952d1 htdocs/admin/index.bml
--- a/htdocs/admin/index.bml	Thu Oct 28 10:15:22 2010 +0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-<?_c
-#
-# admin/index.bml
-#
-# Because blank index files annoy me. Lists all pages $remote has privs
-# to view; does not show the link if you can't use it.
-#
-# Authors:
-#      Denise Paolucci <denise@dreamwidth.org>
-#      Sophie Hamilton <dw-bugzilla@theblob.org>
-#
-# Copyright (c) 2009 by Dreamwidth Studios, LLC.
-#
-# This program is free software; you may redistribute it and/or modify it under
-# the same terms as Perl itself. For a copy of the license, please reference
-# 'perldoc perlartistic' or 'perldoc perlgpl'.
-#
-_c?><?page
-body<=
-<?_code
-{
-    use strict;
-    use vars qw/ %GET %POST $title $windowtitle @errors @warnings /;
-
-    # translated/custom page title can go here
-    $title = "<?_ml .admin.title _ml?>";
-    $windowtitle = "<?_ml .admin.title _ml?>";
-
-    # for pages that require authentication
-    my $remote = LJ::get_remote();
-    return "<?needlogin?>" unless $remote;
-
-    my $ret;
-
-    my @adminpages = (
-        [ 'capedit',
-            '<?_ml .admin.capability.link _ml?>', '<?_ml .admin.capability.text _ml?>', [ 'admin:*', sub {
-                return ( $LJ::IS_DEV_SERVER, "<?_ml .devserver _ml?>" );
-            } ] ],
-        [ 'clusterstatus',
-            '<?_ml .admin.cluster.link _ml?>', '<?_ml .admin.cluster.text _ml?>', [ 'supporthelp' ] ],
-        [ 'console/',
-            '<?_ml .admin.console.link _ml?>', '<?_ml .admin.console.text _ml?>' ],
-        [ 'schema/',
-            '<?_ml .admin.dbschema.link _ml?>', '<?_ml .admin.dbschema.text _ml?>', [ 'schemadoc' ] ],
-        [ 'dupkiller',
-            '<?_ml .admin.dupkiller.link _ml?>', '<?_ml .admin.dupkiller.text _ml?>', [ 'supporthelp' ] ],
-        [ 'entryprops',
-            '<?_ml .admin.entryprops.link _ml?>', '<?_ml .admin.entryprops.text _ml?>', [ 'canview:entryprops', 'canview:*', sub {
-                return ( $LJ::IS_DEV_SERVER, "<?_ml .devserver _ml?>" );
-            } ] ],
-        [ 'faq/',
-            '<?_ml .admin.faq.link _ml?>', '<?_ml .admin.faq.text _ml?>', [ 'faqadd', 'faqedit', 'faqcat' ] ],
-        [ 'fileedit/',
-            '<?_ml .admin.file_edit.link _ml?>', 'Allows you to edit various include files.', [ 'fileedit' ] ],
-        [ 'invites/', '<?_ml .admin.invites.link _ml?>', '<?_ml .admin.invites.text _ml?>', [ 'finduser:codetrace', 'finduser:*', 'payments' ] ],
-        [ 'logout_user',
-            '<?_ml .admin.logout_user.link _ml?>', '<?_ml .admin.logout_user.text _ml?>', [ 'suspend' ] ],
-        [ 'memcache',
-            '<?_ml .admin.memcache.link _ml?>', '<?_ml .admin.memcache.text _ml?>', [ 'siteadmin:memcacheview', 'siteadmin:*' ] ],
-        [ 'memcache_view',
-            '<?_ml .admin.memcache_view.link _ml?>', '<?_ml .admin.memcache_view.text _ml?>', [ 'siteadmin:memcacheview', 'siteadmin:*', sub {
-                return ( $LJ::IS_DEV_SERVER, "<?_ml .devserver _ml?>" );
-            } ] ],
-        [ 'mysql_status',
-            '<?_ml .admin.mysql.link _ml?>', '<?_ml .admin.mysql.text _ml?>', [ 'siteadmin:mysqlstatus', 'siteadmin:*' ] ],
-        [ 'navtag',
-            '<?_ml .admin.navtag.link _ml?>', '<?_ml .admin.navtag.text _ml?>', [ 'siteadmin:navtag', 'siteadmin:*' ] ],
-        [ 'pay/',
-            '<?_ml .admin.pay.link _ml?>', '<?_ml .admin.pay.text _ml?>', [ 'payments' ] ],
-        [ 'priv/',
-            '<?_ml .admin.priv.link _ml?>', '<?_ml .admin.priv.text _ml?>' ],
-        [ 'propedit',
-            '<?_ml .admin.propedit.link _ml?>', '<?_ml .admin.propedit.text _ml?>', [ 'canview:userprops', 'canview:*' ] ],
-        [ 'recent_comments',
-            '<?_ml .admin.recent_comments.link _ml?>', '<?_ml .admin.recent_comments.text _ml?>', [ 'siteadmin:commentview', 'siteadmin:*' ] ],
-        [ 'sitemessages/add',
-            '<?_ml .admin.sitemessages_add.link _ml?>', '<?_ml .admin.sitemessages_add.text _ml?>', [ 'siteadmin:sitemessages', 'siteadmin:*', sub {
-                return ( $LJ::IS_DEV_SERVER, "<?_ml .devserver _ml?>" );
-            } ] ],
-        [ 'sitemessages/manage',
-            '<?_ml .admin.sitemessages_manage.link _ml?>', '<?_ml .admin.sitemessages_manage.text _ml?>', [ 'siteadmin:sitemessages', 'siteadmin:*', sub {
-                return ( $LJ::IS_DEV_SERVER, "<?_ml .devserver _ml?>" );
-            } ] ],
-        [ 'spamreports',
-            '<?_ml .admin.spamreports.link _ml?>', '<?_ml .admin.spamreports.text _ml?>', [ 'siteadmin:spamreports', 'siteadmin:*' ] ],
-        [ 'stats',
-            '<?_ml .admin.stats.link _ml?>', '<?_ml .admin.stats.text _ml?>', [ 'payments' ] ],
-        [ 'statushistory',
-            '<?_ml .admin.statushistory.link _ml?>', '<?_ml .admin.statushistory.text _ml?>', [ 'historyview', sub {
-                return ( $LJ::IS_DEV_SERVER, "<?_ml .devserver _ml?>" );
-            } ] ],
-        [ 'styleinfo',
-            '<?_ml .admin.styleinfo.link _ml?>', '<?_ml .admin.styleinfo.text _ml?>', [ sub {
-                return ( LJ::Support::has_any_support_priv($remote), "<?_ml .anysupportpriv _ml?>" );
-            }, sub {
-                return ( $LJ::IS_DEV_SERVER, "<?_ml .devserver _ml?>" );
-            } ] ],
-        [ 'sysban',
-            '<?_ml .admin.sysban.link _ml?>', '<?_ml .admin.sysban.text _ml?>', [ 'sysban' ] ],
-        [ 'theschwartz',
-            '<?_ml .admin.theschwartz.link _ml?>', '<?_ml .admin.theschwartz.text _ml?>', [ 'siteadmin:theschwartz' ] ],
-        [ 'translate/',
-            '<?_ml .admin.translate.link _ml?>', '<?_ml .admin.translate.text _ml?>' ],
-        [ 'userlog',
-            '<?_ml .admin.userlog.link _ml?>', '<?_ml .admin.userlog.text _ml?>', [ 'canview:userlog', 'canview:*' ] ],
-    );
-
-    $ret .= "<ul>\n";
-
-    my $adminstar = $remote && $remote->has_priv( 'admin', '*' );
-    foreach my $page ( @adminpages ) {
-        my ( $path, $name, $description, $privs ) = @{$page};
-        my $showpage = 0;
-        my ( @needsprivs, @gotprivs );
-        my $haspriv = 0;
-        foreach my $priv ( @{$privs} ) {
-            my $result = ( ref( $priv ) eq "CODE" ?
-                             ( $priv->() )[0] :
-                             $remote && $remote->has_priv( split( /:/, $priv ) ) );
-            my $displayedpriv = ( ref( $priv ) eq "CODE" ? ( $priv->() )[1] : $priv );
-            push( @gotprivs,   $displayedpriv ) if $result;
-            push( @needsprivs, $displayedpriv ) if !$result;
-            $haspriv  = 1 if $result;
-            $showpage = 1 if $adminstar || $result;
-        }
-        if ( @{$privs} == 0 ) {
-            $showpage = 1;
-            $haspriv  = 1;
-        }
-        if ( $showpage ) {
-            my $needspriv = ( $haspriv ? "" : " needspriv" );
-            my $privreason = "";
-            if ( @gotprivs || @needsprivs ) {
-                my $needpriv = ( @needsprivs > 1 ? "<?_ml .needs_one_of _ml?>" : "<?_ml .needspriv _ml?>" );
-                $privreason = ( $haspriv ? "(<b>"  . join( "</b>, <b>", @gotprivs   ) . "</b>)"
-                                         : "($needpriv: <b>" . join( "</b>, <b>", @needsprivs ) . "</b>)" );
-            }
-            $ret .= "<li class='item$needspriv'><div class='itemhead'><a href='$path'>$name</a> <span class='itemprivs'>$privreason</span></div><div class='itemdef'>$description</div></li>\n";
-        }
-    }
-
-    $ret .= "</ul>";
-
-    return $ret;
-}
-_code?>
-<=body
-title=><?_code return $title; _code?>
-windowtitle=><?_code return $windowtitle; _code?>
-head<=
-<style type="text/css">
-.item {margin-bottom: 15px;}
-.item.needspriv div, .item.needspriv a {color: #A0A0A0;}
-.itemhead {font-size: bigger; font-weight: bold;}
-.itemdef {margin-left: 2em;}
-.itemprivs {font-size: smaller; font-weight: normal; color: #707070;}
-</style>
-<=head
-page?>
diff -r f4fcc06b2940 -r 61913b5952d1 htdocs/admin/index.bml.text
--- a/htdocs/admin/index.bml.text	Thu Oct 28 10:15:22 2010 +0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-;; -*- coding: utf-8 -*-
-
-.admin.title=Admin Tools
-
-.admin.capability.link=Capability Edit
-.admin.capability.text=For editing user capabilities.
-
-.admin.cluster.link=Cluster Status
-.admin.cluster.text=Get information on cluster availability.
-
-.admin.console.link=Console
-.admin.console.text=For general input; usable by all users to an extent.
-
-.admin.dbschema.link=Database Schema
-.admin.dbschema.text=Shows the database schema.
-
-.admin.dupkiller.link=Duplicate Entry Killer
-.admin.dupkiller.text=Checks for (and kills) duplicate entries.
-
-.admin.entryprops.link=Entry Properties
-.admin.entryprops.text=View the properties set on a particular entry.
-
-.admin.faq.link=FAQ Tools
-.admin.faq.text=Allows you to manipulate the FAQ.
-
-.admin.file_edit.link=File Edit
-.admin.file_edit.text=Allows you to edit various include files.
-
-.admin.invites.link=Invite Code Management
-.admin.invites.text=View and manage invite codes.
-
-.admin.logout_user.link=Logout User
-.admin.logout_user.text=Logs a user out of the site.
-
-.admin.memcache.link=Memcache Overview
-.admin.memcache.text=Shows current memcache conditions.
-
-.admin.memcache_view.link=Memcache View
-.admin.memcache_view.text=Shows current memcache details.
-
-.admin.mysql.link=MySQL Status
-.admin.mysql.text=Shows current MySQL status.
-
-.admin.navtag.link=Navtag Edit
-.admin.navtag.text=Allows you to tag pages for navigation.
-
-.admin.pay.link=Payment Managements
-.admin.pay.text=Review payment details.
-
-.admin.priv.link=Privilege Management
-.admin.priv.text=View privs by priv or by user. Some priv lists are private.
-
-.admin.propedit.link=User Property Edit
-.admin.propedit.text=Allows you to view and edit userprops.
-
-.admin.recent_comments.link=Recent Comments
-.admin.recent_comments.text=Allows you to view a user's recent comments.
-
-.admin.sitemessages_add.link=Site Messages - Add
-.admin.sitemessages_add.text=Add new site-wide messages.
-
-.admin.sitemessages_manage.link=Site Messages - Manage
-.admin.sitemessages_manage.text=View and manipulate site-wide messages.
-
-.admin.spamreports.link=Spam Reports
-.admin.spamreports.text=View and handle reports of spam.
-
-.admin.stats.link=Business Statistics
-.admin.stats.text=Detailed breakdown of business statistics
-
-.admin.statushistory.link=Statushistory
-.admin.statushistory.text=Shows you a user's statushistory.
-
-.admin.styleinfo.link=Style Info
-.admin.styleinfo.text=Shows you a user's style information.
-
-.admin.sysban.link=Sysban Management
-.admin.sysban.text=Set and manage sysbans.
-
-.admin.theschwartz.link=TheSchwartz Queue/Error Viewer
-.admin.theschwartz.text=View the status of jobs in the TheSchwartz queue.
-
-.admin.translate.link=Translation & Site Copy
-.admin.translate.text=View and edit the site copy and translations.
-
-.admin.userlog.link=Userlog Viewer
-.admin.userlog.text=Shows you a user's logged actions.
-
-.anysupportpriv=any support priv
-.devserver=dev server
-
-.needspriv=needs
-.needs_one_of=needs one of
-
-
-
-
-
-
-
diff -r f4fcc06b2940 -r 61913b5952d1 t/routing.t
--- a/t/routing.t	Thu Oct 28 10:15:22 2010 +0800
+++ b/t/routing.t	Thu Oct 28 11:06:03 2010 +0800
@@ -1,6 +1,6 @@
 # -*-perl-*-
 use strict;
-use Test::More tests => 186;
+use Test::More tests => 194;
 use lib "$ENV{LJHOME}/cgi-bin";
 
 # don't let DW::Routing load DW::Controller subclasses
@@ -217,7 +217,37 @@ handle_request( "/test app implied_forma
 handle_request( "/test app implied_format (app)" , "/test/app/all_format.json", 1, "it_worked_app_af" ); # 3 test
 # 186
 
-use Data::Dumper;
+DW::Routing->register_string( "/xx3/index", \&handler, app => 1, args => "it_worked_redir" );
+
+$expected_format = 'html';
+handle_request( "/xx3" , "/xx3/", 1, "it_worked_redir" ); # 3 tests
+# 189
+
+handle_request( "/xx3" , "/xx3/index", 1, "it_worked_redir" ); # 3 tests
+# 192
+
+handle_redirect( '/xx3', '/xx3/' );
+# 194
+
+sub handle_redirect {
+    my ( $uri, $expected ) = @_;
+
+    $DW::Request::determined = 0;
+    $DW::Request::cur_req = undef;
+
+    my $req = HTTP::Request->new(GET=>"$uri");
+
+    my $opts = DW::Routing->get_call_opts( uri => $uri );
+
+    return fail( "Opts is undef" ) unless $opts;
+
+    my $hash = $opts->call_opts;
+    return fail( "No call opts" ) unless $hash && $hash->{sub};
+
+    is( $hash->{sub}, \&DW::Routing::_redirect_helper );
+    is( $hash->{args}, $expected );
+}
+
 sub handle_request {
     my ( $name, $uri, $valid, $expected, %opts ) = @_;
 
diff -r f4fcc06b2940 -r 61913b5952d1 views/admin/index.tt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/views/admin/index.tt	Thu Oct 28 11:06:03 2010 +0800
@@ -0,0 +1,37 @@
+[%# admin/index.tt
+
+Admin action list index pages
+
+Authors:
+      Andrea Nall <anall@andreanall.com>
+      Denise Paolucci <denise@dreamwidth.org>
+      Sophie Hamilton <dw-bugzilla@theblob.org>
+
+This program is free software; you may redistribute it and/or modify it under
+the same terms as Perl itself.  For a copy of the license, please reference
+'perldoc perlartistic' or 'perldoc perlgpl'.
+
+%][%- sections.title = title_ml | ml -%]
+[%- sections.head = BLOCK %]
+<style type="text/css">
+.item {margin-bottom: 15px;}
+.item.needspriv div, .item.needspriv a {color: #A0A0A0;}
+.itemhead {font-size: bigger; font-weight: bold;}
+.itemdef {margin-left: 2em;}
+.itemprivs {font-size: smaller; font-weight: normal; color: #707070;}
+</style>
+[%- END -%]
+
+[%- IF ml_scope -%][%- CALL dw.ml_scope( ml_scope ) -%][%- END -%]
+[%- IF description_ml -%]<p>[%- description_ml | ml -%][%- END -%]
+
+[%- FOREACH p IN pages -%][%- p.link = p.link_ml | ml -%][%- END -%]
+<ul>[% FOREACH p IN pages.sort_by_key( 'link' ) %]
+<li class='item[%- p.haspriv ? "" : " needspriv" -%]'><div class='itemhead'><a href='[%- p.path -%]'>[%- p.link -%]</a> [%- IF p.gotprivs.size OR p.needsprivs.size -%]<span class='itemprivs'>
+([%- IF p.haspriv -%]
+<b>[%- p.gotprivs.join("</b>, <b>") -%]</b>
+[%- ELSE -%]
+<b>[%- ( p.needsprivs.size > 1 ? '.needs_one_of' : '.needspriv' ) | ml -%]: [%- p.needsprivs.join("</b>, <b>") -%]</b>
+[%- END -%])
+</span>[%- END -%]</div><div class='itemdef'>[%- p.description_ml | ml -%]</div></li>
+[% END %]</ul>
diff -r f4fcc06b2940 -r 61913b5952d1 views/admin/index.tt.text
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/views/admin/index.tt.text	Thu Oct 28 11:06:03 2010 +0800
@@ -0,0 +1,97 @@
+;; -*- coding: utf-8 -*-
+
+.admin.title=Admin Tools
+
+.admin.capability.link=Capability Edit
+.admin.capability.text=For editing user capabilities.
+
+.admin.cluster.link=Cluster Status
+.admin.cluster.text=Get information on cluster availability.
+
+.admin.console.link=Console
+.admin.console.text=For general input; usable by all users to an extent.
+
+.admin.dbschema.link=Database Schema
+.admin.dbschema.text=Shows the database schema.
+
+.admin.dupkiller.link=Duplicate Entry Killer
+.admin.dupkiller.text=Checks for (and kills) duplicate entries.
+
+.admin.entryprops.link=Entry Properties
+.admin.entryprops.text=View the properties set on a particular entry.
+
+.admin.faq.link=FAQ Tools
+.admin.faq.text=Allows you to manipulate the FAQ.
+
+.admin.file_edit.link=File Edit
+.admin.file_edit.text=Allows you to edit various include files.
+
+.admin.invites.link=Invite Code Management
+.admin.invites.text=View and manage invite codes.
+
+.admin.logout_user.link=Logout User
+.admin.logout_user.text=Logs a user out of the site.
+
+.admin.memcache.link=Memcache Overview
+.admin.memcache.text=Shows current memcache conditions.
+
+.admin.memcache_view.link=Memcache View
+.admin.memcache_view.text=Shows current memcache details.
+
+.admin.mysql.link=MySQL Status
+.admin.mysql.text=Shows current MySQL status.
+
+.admin.navtag.link=Navtag Edit
+.admin.navtag.text=Allows you to tag pages for navigation.
+
+.admin.pay.link=Payment Managements
+.admin.pay.text=Review payment details.
+
+.admin.priv.link=Privilege Management
+.admin.priv.text=View privs by priv or by user. Some priv lists are private.
+
+.admin.propedit.link=User Property Edit
+.admin.propedit.text=Allows you to view and edit userprops.
+
+.admin.recent_comments.link=Recent Comments
+.admin.recent_comments.text=Allows you to view a user's recent comments.
+
+.admin.sitemessages_add.link=Site Messages - Add
+.admin.sitemessages_add.text=Add new site-wide messages.
+
+.admin.sitemessages_manage.link=Site Messages - Manage
+.admin.sitemessages_manage.text=View and manipulate site-wide messages.
+
+.admin.spamreports.link=Spam Reports
+.admin.spamreports.text=View and handle reports of spam.
+
+.admin.statushistory.link=Statushistory
+.admin.statushistory.text=Shows you a user's statushistory.
+
+.admin.styleinfo.link=Style Info
+.admin.styleinfo.text=Shows you a user's style information.
+
+.admin.sysban.link=Sysban Management
+.admin.sysban.text=Set and manage sysbans.
+
+.admin.theschwartz.link=TheSchwartz Queue/Error Viewer
+.admin.theschwartz.text=View the status of jobs in the TheSchwartz queue.
+
+.admin.translate.link=Translation & Site Copy
+.admin.translate.text=View and edit the site copy and translations.
+
+.admin.userlog.link=Userlog Viewer
+.admin.userlog.text=Shows you a user's logged actions.
+
+.anysupportpriv=any support priv
+.devserver=dev server
+
+.needspriv=needs
+.needs_one_of=needs one of
+
+
+
+
+
+
+
diff -r f4fcc06b2940 -r 61913b5952d1 views/admin/rename.tt
--- a/views/admin/rename.tt	Thu Oct 28 10:15:22 2010 +0800
+++ b/views/admin/rename.tt	Thu Oct 28 11:06:03 2010 +0800
@@ -9,8 +9,9 @@ the same terms as Perl itself.  For a co
 the same terms as Perl itself.  For a copy of the license, please reference
 'perldoc perlartistic' or 'perldoc perlgpl'.
 %]
+[%- sections.title = '.title' | ml -%]
 
-[% sections.title = '.title' | ml %]
+<a href="[%site.root%]/admin/rename/new">[% '.new.link' | ml %]</a>
 
 [% IF renames %]
     [% IF renames.size == 0 %]
@@ -35,7 +36,7 @@ the same terms as Perl itself.  For a co
 
 [% IF user %]
 <p>
-<a href="[%site.root%]/admin/rename">Return to renames lookup</a>
+<a href="[%site.root%]/admin/rename/">Return to renames lookup</a>
 </p>
 [% ELSE %]
 <form method="GET">
diff -r f4fcc06b2940 -r 61913b5952d1 views/admin/rename.tt.text
--- a/views/admin/rename.tt.text	Thu Oct 28 10:15:22 2010 +0800
+++ b/views/admin/rename.tt.text	Thu Oct 28 11:06:03 2010 +0800
@@ -1,4 +1,9 @@
 ;; -*- coding: utf-8 -*-
+
+.admin.link=Renames
+.admin.text=Manage renames
+
+.new.link=Create Rename
 
 .renames.list.empty=No renames involving "[[user]]".
 
diff -r f4fcc06b2940 -r 61913b5952d1 views/admin/stats.tt.text
--- a/views/admin/stats.tt.text	Thu Oct 28 10:15:22 2010 +0800
+++ b/views/admin/stats.tt.text	Thu Oct 28 11:06:03 2010 +0800
@@ -1,3 +1,7 @@
 ;; -*- coding: utf-8 -*-
 
+.admin.link=Business Statistics
+.admin.text=Detailed breakdown of business statistics
+
 .title=Business Statistics
+
--------------------------------------------------------------------------------