[Rt-commit] rt branch, remove-delegation, created. rt-3.8.8-719-ga2f5c42

Jesse Vincent jesse at bestpractical.com
Tue Sep 7 13:46:11 EDT 2010


The branch, remove-delegation has been created
        at  a2f5c421711acf53426c23cc763d87af6976eb4b (commit)

- Log -----------------------------------------------------------------
commit df8d7d0de45497c74c1a7eb269c2cb9e7f6c3f55
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Wed Sep 1 14:40:20 2010 -0400

    Initial import of the mobile UI extension as a core feature

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 90e3f3f..b5e60a8 100755
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -656,6 +656,21 @@ sub SendStaticFile {
     close $fh;
 }
 
+
+
+sub MobileClient {
+    my $self = shift;
+
+
+if (($ENV{'HTTP_USER_AGENT'} || '') =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS)/io && !$HTML::Mason::Commands::session{'NotMobile'})  {
+    return 1;
+} else {
+    return undef;
+}
+
+}
+
+
 sub StripContent {
     my %args    = @_;
     my $content = $args{Content};
diff --git a/share/html/Elements/Login b/share/html/Elements/Login
index 00a8ab8..94d658e 100755
--- a/share/html/Elements/Login
+++ b/share/html/Elements/Login
@@ -46,12 +46,16 @@
 %# 
 %# END BPS TAGGED BLOCK }}}
 <%INIT>
-if ($m->request_comp->path =~ '^/REST/\d+\.\d+/') {
+if ( $m->request_comp->path =~ '^/REST/\d+\.\d+/' ) {
     $r->content_type("text/plain");
     $m->error_format("text");
     $m->out("RT/$RT::VERSION 401 Credentials required\n");
     $m->out("\n$Error\n") if $Error;
     $m->abort;
+} elsif (    RT::Interface::Web->MobileClient()
+          || ( $m->request_comp->path() =~ m{^/m(?:\/|$)} ) ) {
+    $m->comp( '/m / login ', %ARGS );
+    $m->abort;
 }
 
 my $req_uri;
diff --git a/share/html/Ticket/Display.html b/share/html/Ticket/Display.html
index 0e6d761..a273df5 100755
--- a/share/html/Ticket/Display.html
+++ b/share/html/Ticket/Display.html
@@ -100,6 +100,12 @@ $Collapsed => undef
 
 $m->callback( TicketObj => $TicketObj, ARGSRef => \%ARGS, CallbackName => 'Initial' );
 
+if ( ! $ARGS{'NoRedirect'} && RT::Interface::Web->MobileClient()) {
+    RT::Interface::Web::Redirect(RT->Config->Get('WebURL').'/m/ticket/show.html?id='.$TicketObj->id);
+    $m->abort;
+}
+
+
 my (@Actions, $Tickets, $title);
 
 
diff --git a/share/html/index.html b/share/html/index.html
index 60eb0a5..2c3c10b 100755
--- a/share/html/index.html
+++ b/share/html/index.html
@@ -86,6 +86,11 @@ If you need commercial support, please contact us at sales at bestpractical.com.
 <& /Elements/MyRT &>
 <%init>
 
+if ( RT::Interface::Web->MobileClient()) {
+    RT::Interface::Web::Redirect(RT->Config->Get('WebURL') ."m/");
+    $m->abort();
+}
+
 my @results;
 my $skip_create = 0;
 
diff --git a/share/html/m/_elements/footer b/share/html/m/_elements/footer
new file mode 100644
index 0000000..2c7602d
--- /dev/null
+++ b/share/html/m/_elements/footer
@@ -0,0 +1,8 @@
+  <div id="bpscredits">
+    <& /Elements/Logo, ShowName => 0 &>
+    <div id="copyright">
+<&|/l,     '', '', '2010', '<a href="http://www.bestpractical.com?rt='.$RT::VERSION.'">Best Practical Solutions, LLC</a>', &>[_1] RT [_2] Copyright 1996-[_3] [_4].</&>
+</div>
+</div>
+</body>
+</html>
diff --git a/share/html/m/_elements/full_site_link b/share/html/m/_elements/full_site_link
new file mode 100644
index 0000000..c17dfdb
--- /dev/null
+++ b/share/html/m/_elements/full_site_link
@@ -0,0 +1 @@
+<a id="fullsite" href="<%RT->Config->Get('WebPath')%>/m?NotMobile=1"><&|/l&>Not using a mobile browser?</&></a>
diff --git a/share/html/m/_elements/header b/share/html/m/_elements/header
new file mode 100644
index 0000000..2c20383
--- /dev/null
+++ b/share/html/m/_elements/header
@@ -0,0 +1,24 @@
+<%args>
+$title => undef
+$show_home_button => 1
+</%args>
+<%init>
+$r->headers_out->{'Pragma'} = 'no-cache';
+$r->headers_out->{'Cache-control'} = 'no-cache';
+</%init>
+<html>
+<head>
+<link rel="stylesheet" type="text/css" href="<%RT->Config->Get('WebPath')|n%>/m/style.css"/>
+<title><%$title%></title>
+<meta name="viewport" content="width=device-width height=device-height user-scalable=yes"/>
+</head>
+<body>
+% if ($show_home_button) {
+% # The align is for older browsers, like the blackberry
+<div id="gohome" align="right">
+<a href="<%RT->Config->Get('WebPath')%>/m/"><&|/l&>Homepage</&></a>
+</div>
+% }
+% if ($title) {
+<h1><%$title%></h1>
+% }
diff --git a/share/html/m/_elements/menu b/share/html/m/_elements/menu
new file mode 100644
index 0000000..e13c1c7
--- /dev/null
+++ b/share/html/m/_elements/menu
@@ -0,0 +1,62 @@
+<&| /Widgets/TitleBox, class => 'menu'&>
+<ul class="menu">
+% for my $item (@menu) {
+% if (exists $item->{html}) {
+<li><%$item->{html} |n%></li>
+% } else {
+<li><a href="<%RT->Config->Get('WebPath')%><%$item->{url}%>"><%$item->{label}%></a></li>
+% }
+% }
+</ul>
+</&>
+<%init>
+use RT::SavedSearches;
+my @menu = (
+    {         html => '<form method="GET" id="search" action="'
+            . RT->Config->Get('WebPath')
+            . '/m/tickets/search">'
+            . loc("Search")
+            . ': <input type="text" name="q" id="q" value=""/>'
+            . '<input type="submit" value="'
+            . loc("Go")
+            . '"/></form>'
+    },
+    {   label => loc("New ticket"),
+        url   => '/m/ticket/select_create_queue',
+    },
+    {   label => loc("Bookmarked tickets"),
+        url   => '/m/tickets/search?name=Bookmarked%20Tickets',
+    },
+    {   label => loc("Tickets I own"),
+        url   => '/m/tickets/search?name=My%20Tickets',
+    },
+    {   label => loc("Unowned tickets"),
+        url   => '/m/tickets/search?name=Unowned%20Tickets',
+    },
+    {   label => loc("All tickets"),
+        url   => '/m/tickets/search?query=id!%3d0&order_by=id&order=DESC'
+    },
+);
+
+
+if ( $session{'CurrentUser'}->HasRight( Right  => 'LoadSavedSearch', Object => $RT::System))
+    {
+
+        my @Objects = RT::SavedSearches->new( $session{CurrentUser} )->_PrivacyObjects;
+        push @Objects, RT::System->new( $session{'CurrentUser'} )
+            if $session{'CurrentUser'}->HasRight(
+            Object => $RT::System,
+            Right  => 'SuperUser'
+            );
+
+        foreach my $object (@Objects) {
+            my @searches = $object->Attributes->Named('SavedSearch');
+            foreach my $search (@searches) {
+                next unless $search->SubValue("SearchType") eq 'Ticket';
+                push @menu, { label => $search->Description, url => '/m/tickets/search?query=' . $search->SubValue("Query").'&order='.$search->SubValue("Order").'&order_by='.$search->SubValue("OrderBy") };
+
+            }
+        }
+}
+push @menu,  {   label => loc("Logout"), url   => '/m/logout', } ;
+</%init>
diff --git a/share/html/m/_elements/raw_style b/share/html/m/_elements/raw_style
new file mode 100644
index 0000000..c97a900
--- /dev/null
+++ b/share/html/m/_elements/raw_style
@@ -0,0 +1,415 @@
+body {
+    font-family: helvetica, arial, sans-serif;
+    background-color: #ccf;
+    margin: 0;
+}
+
+h1 { 
+    font-size: 1.2em;
+    padding-top: 0.5em;
+    padding-left: 0.2em;
+    display: block; 
+    background-color: #fff;
+
+}
+
+div.buttons {
+    text-align: right;
+    padding-right: 0.5em;
+    padding-bottom: 0.5em;
+}
+
+.titlebox-title {
+    font-size: 1.1em;
+    margin-left: 0.5em;
+    margin-top: -1.2em;
+    top: -0.5em;
+    padding: 0.5em;
+    position: relative;
+    display: inline-block;
+    text-decoration: none;
+    background-color: #fff;
+    -moz-border-radius: 0.25em;
+    -webkit-border-radius: 0.25em;
+    -webkit-box-shadow: #333 0px 0px 5px;
+    -moz-box-shadow: #333 0px 0px 5px;
+    box-shadow: #333 0px 0px 5px;
+}
+
+ul.menu
+{
+    text-align: left;
+    list-style: none;
+    padding: 0;
+    margin: -0.6em;
+    left: 0;
+}
+
+ul.menu li
+{
+    display: block;
+    margin: 0;
+    padding: 0;
+    font-weight: bold;
+}
+
+ul.ticketlist li:active, ul.ticketlist li:hover,
+ul.menu li:active, ul.menu li:hover {
+    background-color: #eee;
+}
+
+
+ul.menu li
+{
+    display: block;
+    padding: 1em;
+    margin: 0;
+    border:0;
+    border-top-width: 1px;
+    border-top-color: #666;
+    border-style: solid;
+    text-decoration: none;
+}
+
+ul.menu li:first-child{
+    border: none;
+}
+
+ul.menu li#active a
+{
+    color: #800000;
+}
+
+div.titlebox, #bpscredits, .ticket_menu{
+    -moz-border-radius: 1em;
+    -webkit-border-radius: 1em;
+    margin: 0.5em;
+    background-color: #fff;
+    padding-top: 1em;
+    padding-bottom: 0.8em;
+    margin-top: 1.25em;
+    -webkit-box-shadow: #333 0px 0px 5px;
+    -moz-box-shadow: #333 0px 0px 5px;
+    box-shadow: #333 0px 0px 5px;
+    margin-bottom: 1em;
+}
+
+div .titlebox-content {
+    padding-left: 0.5em;
+    padding-right: 0.5em;
+}
+
+hr.clear {
+    display: none;
+}
+
+
+.label, .labeltop {
+    font-weight: normal;
+}
+.value { 
+    font-weight: bold;
+    display:inline-block;
+}
+
+ul.ticketlist {
+    list-style: none;
+    padding-left: -0.5em;
+    padding-right: -0.5em; /* to counteract the titlebox and get shading to the end*/
+    margin-left: -0.5em;
+    margin-right: -0.5em;
+    padding: 0em;
+    padding-bottom: 1em;
+}
+
+ul.ticketlist li.ticket {
+    padding: 0.5em;
+    font-weight: bold;
+    border-bottom: 1px solid #999;
+    
+}
+ul.ticketlist li.ticket:first-child {
+    border-top: 1px solid #999;
+}
+
+ul.ticketlist li.ticket a.ticket{
+    display: inline-block;
+    font-size: 1em;
+    width: 100%;
+    padding: 0.5em;
+    padding-bottom: 5em;
+    margin-bottom: -5em;
+}
+ul.ticketlist li.ticket div.metadata {
+}
+
+
+ul.ticketlist li.ticket div.metadata div {
+    padding: 0.2em;
+    font-size:0.8em;
+    display: block;
+}
+
+ul.ticketlist li.ticket div.metadata .label {
+    display: inline-block;
+    width: 6em;
+    font-size: 0.8em;
+    text-align: right;
+    color: #666;
+}
+
+div#paging {
+    text-align: center;
+}
+
+.ticket-reply .titlebox-title, .titlebox.search .titlebox-title, .titlebox.menu .titlebox-title, .ticket_menu .titlebox-title, .history .titlebox-title, #ticket-create-basics .titlebox-title{
+    display: none;
+}
+
+a {
+    color: #000;
+}
+
+.ticket_menu a, .menu a {
+    text-decoration: none;
+}
+
+ul.menu a {
+    padding: 0.5em;
+    margin-top: -0.5em;
+    margin-bottom: -0.5em;
+    display: inline-block;
+    width: 100%;
+}
+
+ul.menu a:after {
+    color: #666;
+    float: right;
+    content: ">";
+    font-size: 1.5em;
+    padding: 0;
+    margin: 0;
+    padding-right: 1em;
+
+}
+
+ul.menu form {
+    display: inline;
+}
+
+ul.menu form * {
+    display: inline;
+}
+
+
+ul.menu form input[type=text] { 
+    width: 7em;
+}
+
+ul.menu form input{ 
+
+    width: auto;
+    padding: 0.5em;
+    margin: -0.5em;
+    margin-left: 1em;
+}
+
+.ticket_menu {
+    text-align: center;
+}
+
+.ticket_menu ul {
+    display: block;
+    margin: 0;
+    padding: 0;
+}
+
+.ticket_menu ul li {
+
+    display: inline-block;
+    text-align: center;
+    padding-bottom: 0.25em;
+    padding-top: 0.25em;
+    font-size: 1em;
+    width: 28%;
+    padding-right: 0.3em;
+    padding-left: 0.2em;
+    border-right: 1px solid #000;
+}
+.ticket_menu ul li:last-child {
+    padding-right: 0;
+    border-right: 0; 
+}
+
+.ticket-info-reminders table {
+
+    width: 100%;
+}
+
+#ticket-create .label:after {
+   content: ": "; 
+    padding-right: 0.25em;
+
+}
+
+#ticket-create .content-label {
+    width: auto;
+    display: block;
+    text-align: left;
+    
+}
+
+#ticket-show .label, .login-body .label {
+    display: inline-block;
+    text-align: right;
+    width: 6em;
+    padding-right: 0.25em;
+    font-size: 0.8em;
+}
+
+.login-body .value {
+    width: auto;
+}
+
+.history ul.history-list {
+    padding: 0;
+    margin: 0;
+    padding-bottom: 2em;
+}
+
+
+.history ul.history-list li:first-child {
+    border-top: 1px solid #ccc;
+}
+
+.history ul.history-list li {
+    list-style: none;
+    border-bottom: 1px solid #ccc;
+    padding: 0.5em;
+}
+
+.history .age {
+    display: inline-block;
+    min-width: 8em;
+    text-align: right;
+
+}
+
+div#login-box div.titlebox {
+    width: 100%;
+    margin-left:auto;
+    margin-right: auto;
+}
+
+div#login-box input[type=text], div#login-box input[type=password] {
+    width: 100%;
+}
+
+#bpscredits img {
+    padding-bottom: 1em;
+}
+
+
+
+#bpscredits {
+    float: right;
+    text-align: right;
+    width: auto;
+    font-size: 0.8em;
+    padding: 1em;
+}
+
+
+:focus {
+    background-color: #ffc;
+    border-color: #000;
+    border-weight: 3px;
+}
+
+input[type=submit], input[type=button], button, #paging a {
+    border: 2px outset;
+    margin: 0.3em;
+    padding: 0.3em;
+    padding-left: 0.6em;
+    padding-right: 0.6em;
+    -moz-border-radius: 0.5em;
+    -webkit-border-radius: 0.5em;
+    background-color: #006699;
+    color: #fff;
+}
+
+form { 
+
+    margin:0;
+}
+
+#gohome {
+    position: absolute;
+    top: 0;
+    right: 0;
+    border-left: 1px solid black;
+    border-bottom: 1px solid black;
+    -moz-border-radius-bottomleft: 1em;
+    -webkit-border-bottom-left-radius: 1em;
+    padding: 0.5em;
+    background-color: #fff;
+}
+
+#gohome a {
+    font-size: 1em;
+    padding: 0.25em;
+    color: #000;
+}
+
+div.txn-content {
+    
+    font-size:0.8em;
+    padding-left:1em;
+    padding-top:0.5em;
+    margin-top: 0.5em;
+    margin-left: 2em;
+    padding-bottom: 0.5em;
+    border-left: 5px solid #00c;
+
+}
+
+.label {
+    text-align: left;
+    width: 10em;
+    color: #666;
+    display: block;
+    padding-bottom: 0.2em;
+    padding-right: 0.2em;
+    
+}
+
+div.entry, tr.input-row {
+    margin-bottom: 0.25em;
+    padding-bottom: 0.25em;
+    border-bottom: 1px solid #ccc;
+    display: block;
+    width: 100%;
+    min-height: 1em;
+}
+
+
+input, input[type=text], input[type=password], select {
+    width: 100%;
+}
+
+.timefield input {
+    width: 5em;
+}
+
+.timefield select {
+    width: auto;
+}
+
+
+textarea {
+    width: 100%;
+}
+
+a#fullsite {
+    padding-left: 1em;
+}
diff --git a/share/html/m/_elements/ticket_list b/share/html/m/_elements/ticket_list
new file mode 100644
index 0000000..43a54c6
--- /dev/null
+++ b/share/html/m/_elements/ticket_list
@@ -0,0 +1,64 @@
+<%args>
+$order => undef
+$order_by => undef
+$query => ''
+$page => 1
+</%args>
+<%init>
+my $collection = RT::Tickets->new($session{'CurrentUser'});
+$collection->FromSQL($query);
+$collection->RowsPerPage(10);
+$collection->GotoPage($page-1);
+# XXX: ->{'order_by'} is hacky, but there is no way to check if
+# collection is ordered or not
+if ( $order_by) {
+        my @order_by = split /\|/, $order_by;
+        my @order = split /\|/,$order;
+    $collection->OrderByCols(
+        map { { FIELD => $order_by[$_], ORDER => $order[$_] } }
+        ( 0 .. $#order_by )
+    );
+}
+
+
+
+$collection->RedoSearch();
+ 
+if ($page > 1 && ! @{$collection->ItemsArrayRef||[]}) {
+    RT::Interface::Web::Redirect( RT->Config->Get('WebURL')."/m/tickets/search?page=".($page-1)."&query=".$query."&order=$order&order_by=$order_by");
+}
+
+</%init>
+<&| /m/_elements/wrapper, title => 
+loc("Found [quant,_1,ticket]",$collection->Count) &>
+<&|/Widgets/TitleBox, class => 'search'
+&>
+<ul class="ticketlist">
+% while (my $ticket = $collection->Next()) {
+<li class="ticket">
+<a class="ticket" href="<%RT->Config->Get('WebPath')%>/m/ticket/show?id=<%$ticket->id%>"><%$ticket->id%>: <%$ticket->Subject%></a>
+<div class="metadata">
+<%perl>
+
+</%perl>
+<div class="requestors"><span class="label"><&|/l&>Requestors</&>:</span> <& /Ticket/Elements/ShowGroupMembers, Group => $ticket->Requestors, Ticket => $ticket &></div>
+<div class="status"><span class="label"><&|/l&>Status</&>:</span> <%$ticket->Status%></div>
+<div class="owner"><span class="label"><&|/l&>Owner</&>:</span> <& /Elements/ShowUser, User => $ticket->OwnerObj, Ticket => $ticket &></div>
+<div class="created"><span class="label"><&|/l&>Created</&>:</span> <%$ticket->CreatedObj->AgeAsString()%></div>
+% if ($ticket->Priority) {
+<div class="priority"><span class="label"><&|/l&>Priority</&>:</span> <%$ticket->Priority%></div>
+% }
+</div>
+</li>
+% }
+</ul>
+<div id="paging">
+% if ($page > 1) { 
+<a href="<%RT->Config->Get('WebPath')%>/m/tickets/search?page=<%$page-1%>&query=<%$query%>&order=<%$order%>&order_by=<%$order_by%>">Back</a>
+% }
+Page <%$page%>
+
+<a href="<%RT->Config->Get('WebPath')%>/m/tickets/search?page=<%$page+1%>&query=<%$query%>&order=<%$order%>&order_by=<%$order_by%>">Next</a>
+</div>
+</&>
+</&>
diff --git a/share/html/m/_elements/ticket_menu b/share/html/m/_elements/ticket_menu
new file mode 100644
index 0000000..257b066
--- /dev/null
+++ b/share/html/m/_elements/ticket_menu
@@ -0,0 +1,31 @@
+<%args>
+$ticket
+</%args>
+<div class="ticket_menu">
+<ul>
+% for my $item (@menu) {
+<li><a href="<%RT->Config->Get('WebPath')%><%$item->{url}%>"><%$item->{label}%></a></li>
+% }
+</ul>
+</div>
+<%init>
+my @menu = ( 
+{ label =>  loc("Basics"),
+  url => '/m/ticket/show?id='.$ticket->id
+},
+ {
+    label => loc("History"),
+  url => '/m/ticket/history?id='.$ticket->id
+    },
+ #{ label => loc("Modify"), url => '/m/ticket/modify?id='.$ticket->id },
+{
+    label => loc("Reply"),
+    url => '/m/ticket/reply?id='.$ticket->id
+}
+
+
+); 
+
+my $width = int(100/ ($#menu +1))-5;
+
+</%init>
diff --git a/share/html/m/_elements/wrapper b/share/html/m/_elements/wrapper
new file mode 100644
index 0000000..794385d
--- /dev/null
+++ b/share/html/m/_elements/wrapper
@@ -0,0 +1,15 @@
+<%args>
+$title => ''
+$show_home_button => 1
+</%args>
+<%init>
+if ($m->request_args->{'NotMobile'}) {
+    $session{'NotMobile'} = 1;
+    RT::Interface::Web::Redirect(RT->Config->Get('WebURL'));
+    $m->abort();
+}
+$m->comp('header', title => $title, show_home_button => $show_home_button);
+$m->out($m->content);
+$m->comp('footer');
+$m->abort();
+</%init>
diff --git a/share/html/m/dhandler b/share/html/m/dhandler
new file mode 100644
index 0000000..627ec22
--- /dev/null
+++ b/share/html/m/dhandler
@@ -0,0 +1,5 @@
+<%init>
+# deal with users who don't have options indexes set right
+RT::Interface::Web::Redirect(RT->Config->Get('WebURL')."m/index.html");
+$m->abort();
+</%init>
diff --git a/share/html/m/index.html b/share/html/m/index.html
new file mode 100644
index 0000000..5b38125
--- /dev/null
+++ b/share/html/m/index.html
@@ -0,0 +1,4 @@
+<&| _elements/wrapper, title => loc("RT for [_1]",RT->Config->Get('rtname'))&>
+<& _elements/menu &>
+<& _elements/full_site_link &>
+</&>
diff --git a/share/html/m/login b/share/html/m/login
new file mode 100644
index 0000000..3688749
--- /dev/null
+++ b/share/html/m/login
@@ -0,0 +1,84 @@
+<%INIT>
+
+my $req_uri;
+
+if (UNIVERSAL::can($r, 'uri') and $r->uri =~ m{.*/m/(.*)}) {
+    $req_uri = '/m/'.$1;
+}
+
+my $default_path = RT->Config->Get('WebPath') ."/m/";
+
+my $form_action = defined $goto             ? $goto
+                : defined $req_uri          ? $req_uri
+                :                             $default_path
+                ;
+
+# sanitize $form_action
+my $uri = URI->new($form_action);
+
+# You get undef scheme with a relative uri like "/Search/Build.html"
+unless (!defined($uri->scheme) || $uri->scheme eq 'http' || $uri->scheme eq 'https') {
+    $form_action = $default_path;
+}
+
+# Make sure we're logging in to the same domain
+# You can get an undef authority with a relative uri like "index.html"
+my $uri_base_url = URI->new(RT->Config->Get('WebURL')."/m/");
+unless (!defined($uri->authority) || $uri->authority eq $uri_base_url->authority) {
+    $form_action = $default_path;
+}
+</%INIT>
+<&| /m/_elements/wrapper, show_home_button => 0 &>
+<style>
+<& /m/_elements/raw_style &>
+</style>
+<h1><&|/l, RT->Config->Get('rtname') &>RT for [_1]</&></h1>
+<div id="body" class="login-body">
+% if ($Error) {
+<&| "/Widgets/TitleBox", title => loc('Error'), hideable => 0, class => 'error'  &>
+<% $Error %>
+</&>
+% }
+
+
+<div id="login-box">
+<&| /Widgets/TitleBox, title => loc('Login'), hideable => 0 &>
+
+% unless (RT->Config->Get('WebExternalAuth') and !RT->Config->Get('WebFallbackToInternalAuth')) {
+<form id="login" name="login" method="post" action="<% $form_action %>">
+
+<div class="entry">
+    <span class="label"><&|/l&>Username</&>:</span><span class="value"><input name="user" value="<%$user%>" id="user" /></span>
+</div>
+
+<div class="entry">
+    <span class="label"><&|/l&>Password</&>:</span><span class="value"><input type="password" name="pass"/></span>
+</div>
+
+<& /Elements/Submit, Label => loc('Login')&>
+
+% foreach my $key (keys %ARGS) {
+%  if (($key ne 'user') and ($key ne 'pass')) {
+% 	if (ref($ARGS{$key}) =~ /ARRAY/) {
+% 		foreach my $val (@{$ARGS{$key}}) {
+<input type="hidden" class="hidden" name="<%$key %>" value="<% $val %>" />
+% 		}
+% 	}
+%	else {
+<input type="hidden" class="hidden" name="<% $key %>" value="<% $ARGS{$key} %>" />
+% 	}
+%  }
+% }
+</form>
+% }
+</&>
+</div><!-- #login-box -->
+</div><!-- #login-body -->
+<& _elements/full_site_link &>
+</&>
+<%ARGS>
+$user => ""
+$pass => undef
+$goto => undef
+$Error => undef
+</%ARGS>
diff --git a/share/html/m/logout b/share/html/m/logout
new file mode 100644
index 0000000..78878a3
--- /dev/null
+++ b/share/html/m/logout
@@ -0,0 +1,7 @@
+<%init>
+if (keys %session) {
+    tied(%session)->delete;
+    $session{'CurrentUser'} = RT::CurrentUser->new;
+}
+RT::Interface::Web::Redirect(RT->Config->Get('WebURL')."/m");
+</%init>
diff --git a/share/html/m/style.css b/share/html/m/style.css
new file mode 100644
index 0000000..22be0a9
--- /dev/null
+++ b/share/html/m/style.css
@@ -0,0 +1,5 @@
+<%init>
+    $HTML::Mason::Commands::r->content_type('text/css');
+    $m->comp('/m/_elements/raw_style');
+    $m->abort();
+</%init>
diff --git a/share/html/m/ticket/create b/share/html/m/ticket/create
new file mode 100644
index 0000000..7c23194
--- /dev/null
+++ b/share/html/m/ticket/create
@@ -0,0 +1,400 @@
+<%ARGS>
+$QuoteTransaction => undef
+$CloneTicket => undef
+</%ARGS>
+<%init>
+$m->callback( CallbackName => "Init", ARGSRef => \%ARGS );
+my $Queue = $ARGS{Queue};
+
+
+my $showrows = sub {
+    my @pairs = @_;
+
+    while (@pairs) {
+        my $key = shift @pairs;
+        my $val = shift @pairs;
+
+        $m->out("<div class=\"entry\"><span class=\"label\">$key</span><div class=\"value\">$val</div></div>");
+
+    }
+
+};
+
+
+my $CloneTicketObj;
+if ($CloneTicket) {
+    $CloneTicketObj = RT::Ticket->new( $session{CurrentUser} );
+    $CloneTicketObj->Load($CloneTicket)
+        or Abort( loc("Ticket could not be loaded") );
+
+    my $clone = {
+        Requestors => join( ',', $CloneTicketObj->RequestorAddresses ),
+        Cc         => join( ',', $CloneTicketObj->CcAddresses ),
+        AdminCc    => join( ',', $CloneTicketObj->AdminCcAddresses ),
+        InitialPriority => $CloneTicketObj->Priority,
+    };
+
+    $clone->{$_} = $CloneTicketObj->$_()
+        for qw/Owner Subject FinalPriority TimeEstimated TimeWorked
+        Status TimeLeft/;
+
+    $clone->{$_} = $CloneTicketObj->$_->AsString
+        for grep { $CloneTicketObj->$_->Unix }
+        map      { $_ . "Obj" } qw/Starts Started Due Resolved/;
+
+    my $members = $CloneTicketObj->Members;
+    my ( @members, @members_of, @refers, @refers_by, @depends, @depends_by );
+    my $refers = $CloneTicketObj->RefersTo;
+    while ( my $refer = $refers->Next ) {
+        push @refers, $refer->LocalTarget;
+    }
+    $clone->{'new-RefersTo'} = join ' ', @refers;
+
+    my $refers_by = $CloneTicketObj->ReferredToBy;
+    while ( my $refer_by = $refers_by->Next ) {
+        push @refers_by, $refer_by->LocalBase;
+    }
+    $clone->{'RefersTo-new'} = join ' ', @refers_by;
+    if (0) {    # Temporarily disabled
+        my $depends = $CloneTicketObj->DependsOn;
+        while ( my $depend = $depends->Next ) {
+            push @depends, $depend->LocalTarget;
+        }
+        $clone->{'new-DependsOn'} = join ' ', @depends;
+
+        my $depends_by = $CloneTicketObj->DependedOnBy;
+        while ( my $depend_by = $depends_by->Next ) {
+            push @depends_by, $depend_by->LocalBase;
+        }
+        $clone->{'DependsOn-new'} = join ' ', @depends_by;
+
+        while ( my $member = $members->Next ) {
+            push @members, $member->LocalBase;
+        }
+        $clone->{'MemberOf-new'} = join ' ', @members;
+
+        my $members_of = $CloneTicketObj->MemberOf;
+        while ( my $member_of = $members_of->Next ) {
+            push @members_of, $member_of->LocalTarget;
+        }
+        $clone->{'new-MemberOf'} = join ' ', @members_of;
+
+    }
+
+    my $cfs = $CloneTicketObj->QueueObj->TicketCustomFields();
+    while ( my $cf = $cfs->Next ) {
+        my $cf_id     = $cf->id;
+        my $cf_values = $CloneTicketObj->CustomFieldValues( $cf->id );
+        my @cf_values;
+        while ( my $cf_value = $cf_values->Next ) {
+            push @cf_values, $cf_value->Content;
+        }
+        $clone->{"Object-RT::Ticket--CustomField-$cf_id-Value"} = join "\n",
+            @cf_values;
+    }
+
+    for ( keys %$clone ) {
+        $ARGS{$_} = $clone->{$_} if not defined $ARGS{$_};
+    }
+
+}
+
+my @results;
+
+my $title = loc("Create a ticket");
+
+my $QueueObj = new RT::Queue($session{'CurrentUser'});
+$QueueObj->Load($Queue) || Abort(loc("Queue could not be loaded."));
+
+$m->callback( QueueObj => $QueueObj, title => \$title, results => \@results, ARGSRef => \%ARGS );
+
+$QueueObj->Disabled && Abort(loc("Cannot create tickets in a disabled queue."));
+
+my $CFs = $QueueObj->TicketCustomFields();
+
+my $ValidCFs = $m->comp(
+    '/Elements/ValidateCustomFields',
+    CustomFields => $CFs,
+    ARGSRef => \%ARGS
+);
+
+# {{{ deal with deleting uploaded attachments
+foreach my $key (keys %ARGS) {
+    if ($key =~ m/^DeleteAttach-(.+)$/) {
+	delete $session{'Attachments'}{$1};
+    }
+    $session{'Attachments'} = { %{$session{'Attachments'} || {}} };
+}
+# }}}
+
+# {{{ store the uploaded attachment in session
+if ($ARGS{'Attach'}) {			# attachment?
+    my $attachment = MakeMIMEEntity(
+        AttachmentFieldName => 'Attach'
+    );
+
+    my $file_path = Encode::decode_utf8("$ARGS{'Attach'}");
+    $session{'Attachments'} = {
+        %{$session{'Attachments'} || {}},
+	$file_path => $attachment,
+    };
+}
+# }}}
+
+# delete temporary storage entry to make WebUI clean
+unless (keys %{$session{'Attachments'}} and $ARGS{'id'} eq 'new') {
+    delete $session{'Attachments'};
+}
+
+my $checks_failure = 0;
+
+my $gnupg_widget = $m->comp('/Elements/GnuPG/SignEncryptWidget:new', Arguments => \%ARGS );
+$m->comp( '/Elements/GnuPG/SignEncryptWidget:Process',
+    self      => $gnupg_widget,
+    QueueObj  => $QueueObj,
+);
+
+
+if ( !exists $ARGS{'AddMoreAttach'} && ($ARGS{'id'}||'') eq 'new' ) {
+    my $status = $m->comp('/Elements/GnuPG/SignEncryptWidget:Check',
+        self      => $gnupg_widget,
+        Operation => 'Create',
+        QueueObj  => $QueueObj,
+    );
+    $checks_failure = 1 unless $status;
+}
+
+# check email addresses for RT's
+{
+    foreach my $field ( qw(Requestors Cc AdminCc) ) {
+        my $value = $ARGS{ $field };
+        next unless defined $value && length $value;
+
+        my @emails = Email::Address->parse( $value );
+        foreach my $email ( grep RT::EmailParser->IsRTAddress($_->address), @emails ) {
+            push @results, loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email->format, loc($field =~ /^(.*?)s?$/) );
+            $checks_failure = 1;
+            $email = undef;
+        }
+        $ARGS{ $field } = join ', ', map $_->format, grep defined, @emails;
+    }
+}
+
+my $skip_create = 0;
+$m->callback( CallbackName => 'BeforeCreate', ARGSRef => \%ARGS, skip_create => \$skip_create, 
+              checks_failure => $checks_failure, results => \@results );
+
+if ((!exists $ARGS{'AddMoreAttach'}) and (defined($ARGS{'id'}) and $ARGS{'id'} eq 'new')) { # new ticket?
+    if ( $ValidCFs && !$checks_failure && !$skip_create ) {
+        $m->comp('show', %ARGS);
+        $RT::Logger->crit("After display call; error is $@");
+        $m->abort();
+    }
+    elsif ( !$ValidCFs ) {
+        # Invalid CFs
+        while (my $CF = $CFs->Next) {
+            my $msg = $m->notes('InvalidField-' . $CF->Id) or next;
+            push @results, $CF->Name . ': ' . $msg;
+        }
+    }
+}
+
+
+
+
+</%init>
+<&| /m/_elements/wrapper, title => $title &>
+<& /Elements/ListActions, actions => \@results  &>
+<form action="<% RT->Config->Get('WebPath') %>/m/ticket/create" method="post" enctype="multipart/form-data" name="TicketCreate" id="ticket-create">
+<input type="hidden" class="hidden" name="id" value="new" />
+% $m->callback( CallbackName => 'FormStart', QueueObj => $QueueObj, ARGSRef => \%ARGS );
+% if ($gnupg_widget) {
+<& /Elements/GnuPG/SignEncryptWidget:ShowIssues, self => $gnupg_widget &>
+% }
+
+
+<div id="ticket-create-simple">
+<&| /Widgets/TitleBox, title => $QueueObj->Name &>
+
+<%perl>
+$showrows->(
+    loc("Subject") => '<input name="Subject" size="30" maxsize="200" value="'.($ARGS{Subject} || '').'" />');
+</%perl>
+    <span class="content-label label"><%loc("Describe the issue below")%></span>
+        <& /Elements/MessageBox, exists $ARGS{Content}  ? (Default => $ARGS{Content}, IncludeSignature => 0 ) : ( QuoteTransaction => $QuoteTransaction ), Height => 5  &>
+
+
+<&/Elements/Submit, Label => loc("Create") &>
+
+
+</&>
+</div>
+
+<div id="ticket-create-basics">
+<&| /Widgets/TitleBox &>
+   <input type="hidden" class="hidden" name="Queue" value="<%$QueueObj->id %>" />
+<%perl>
+
+$showrows->(
+
+   # loc('Queue') => $m->scomp( '/Ticket/Elements/ShowQueue', QueueObj => $QueueObj ) ,
+
+    loc('Status') =>
+
+        $m->scomp(
+        "/Elements/SelectStatus",
+        Name         => "Status",
+        Default      => $ARGS{Status} || 'new',
+        DefaultValue => 0,
+        SkipDeleted  => 1
+        ),
+
+    loc("Owner") =>
+
+        $m->scomp(
+        "/Elements/SelectOwner",
+        Name         => "Owner",
+        QueueObj     => $QueueObj,
+        Default      => $ARGS{Owner} || $RT::Nobody->Id,
+        DefaultValue => 0
+        ),
+
+    loc("Requestors") => $m->scomp(
+        "/Elements/EmailInput",
+        Name    => 'Requestors',
+        Size    => '40',
+        Default => $ARGS{Requestors} || $session{CurrentUser}->EmailAddress
+    ),
+
+    loc("Cc") =>
+
+        $m->scomp( "/Elements/EmailInput", Name => 'Cc', Size => '40', Default => $ARGS{Cc} )
+        . '<span class="comment"><i><font size="-2">'
+        . loc(
+        "(Sends a carbon-copy of this update to a comma-delimited list of email addresses. These people <strong>will</strong> receive future updates.)"
+        )
+        . '</font></i></span>',
+
+    loc("Admin Cc") =>
+
+        $m->scomp( "/Elements/EmailInput", Name => 'AdminCc', Size => '40', Default => $ARGS{AdminCc} )
+        . '<span class="comment" colspan="2"><i><font size="-2">'
+        . loc(
+        "(Sends a carbon-copy of this update to a comma-delimited list of administrative email addresses. These people <strong>will</strong> receive future updates.)"
+        )
+        . '</font></i></span>',
+
+
+);
+
+
+$m->scomp("/Ticket/Elements/EditCustomFields", %ARGS, QueueObj => $QueueObj );
+
+
+$m->scomp("/Ticket/Elements/EditTransactionCustomFields", %ARGS, QueueObj => $QueueObj );
+
+</%perl>
+% if (exists $session{'Attachments'}) {
+
+<%loc("Attached file") %>
+
+<%loc("Check box to delete")%><br />
+% foreach my $attach_name (keys %{$session{'Attachments'}}) {
+<input type="checkbox" class="checkbox" name="DeleteAttach-<%$attach_name%>" value="1" /><%$attach_name%><br />
+% } # end of foreach
+
+
+% } # end of if
+
+<%perl>
+$showrows->(
+    loc("Attach file") =>
+
+        '<div class="value" colspan="5">
+<input type="file" name="Attach" />
+<input type="submit" class="button" name="AddMoreAttach" value="' . loc("Add More Files") . '" />'
+);
+</%perl>
+
+
+% if ( $gnupg_widget ) {
+%$m->scomp("/Elements/GnuPG/SignEncryptWidget", self => $gnupg_widget, QueueObj => $QueueObj )
+% }
+
+
+    <div class="ticket-info-basics">
+	  <&| /Widgets/TitleBox, title => loc('The Basics'), 
+		title_class=> 'inverse',  
+		color => "#993333" &>
+<%perl>
+$showrows->(
+    loc("Priority") => $m->scomp(
+        "/Elements/SelectPriority",
+        Name    => "InitialPriority",
+        Default => $ARGS{InitialPriority} ? $ARGS{InitialPriority} : $QueueObj->InitialPriority,
+    ),
+    loc("Final Priority") => $m->scomp(
+        "/Elements/SelectPriority",
+        Name    => "FinalPriority",
+        Default => $ARGS{FinalPriority} ? $ARGS{FinalPriority} : $QueueObj->FinalPriority,
+    ),
+
+    loc("Time Estimated") => '<span class="timefield">'.$m->scomp(
+        "/Elements/EditTimeValue",
+        Name    => 'TimeEstimated',
+        Default => $ARGS{TimeEstimated} || '',
+        InUnits => $ARGS{'TimeEstimated-TimeUnits'}
+        ).'</span>',
+
+    loc("Time Worked") => '<span class="timefield">'.$m->scomp(
+        "/Elements/EditTimeValue",
+        Name    => 'TimeWorked',
+        Default => $ARGS{TimeWorked} || '',
+        InUnits => $ARGS{'TimeWorked-TimeUnits'}
+    ). '</span>',
+
+    loc("Time Left") => '<span class="timefield">'.$m->scomp(
+        "/Elements/EditTimeValue",
+        Name    => 'TimeLeft',
+        Default => $ARGS{TimeLeft} || '',
+        InUnits => $ARGS{'TimeLeft-TimeUnits'}
+    ).'</span>',
+);
+
+</%perl>
+</&>
+<&|/Widgets/TitleBox, title => loc("Dates"),
+		title_class=> 'inverse',  
+		 color => "#663366"  &>
+
+<%perl>
+$showrows->(
+    loc("Starts") => $m->scomp( "/Elements/SelectDate", Name => "Starts", Default => ( $ARGS{Starts} || '' )),
+    loc("Due")    => $m->scomp( "/Elements/SelectDate", Name => "Due",    Default => ($ARGS{Due}    || '' ))
+);
+
+</%perl>
+</&>
+
+<&|/Widgets/TitleBox, title => loc('Links'), title_class=> 'inverse' &>
+
+<em><%loc("(Enter ticket ids or URLs, separated with spaces)")%></em>
+
+<%perl>
+$showrows->(
+    loc("Depends on")     => '<input size="10" name="new-DependsOn" value="' . ($ARGS{'new-DependsOn'} || '' ). '" />',
+    loc("Depended on by") => '<input size="10" name="DependsOn-new" value="' . ($ARGS{'DependsOn-new'} || '' ) . '" />',
+    loc("Parents")        => '<input size="10" name="new-MemberOf" value="' . ($ARGS{'new-MemberOf'} || '') . '" />',
+    loc("Children")       => '<input size="10" name="MemberOf-new" value="' . ($ARGS{'MemberOf-new'} || '') . '" />',
+    loc("Refers to")      => '<input size="10" name="new-RefersTo" value="' . ($ARGS{'new-RefersTo'} || '') . '" />',
+    loc("Referred to by") => '<input size="10" name="RefersTo-new" value="' . ($ARGS{'RefersTo-new'} || ''). '" />'
+);
+</%perl>
+
+</&>
+
+
+<& /Elements/Submit, Label => loc("Create") &>
+</form>
+</&>
+</&>
diff --git a/share/html/m/ticket/history b/share/html/m/ticket/history
new file mode 100644
index 0000000..a49945d
--- /dev/null
+++ b/share/html/m/ticket/history
@@ -0,0 +1,31 @@
+<%args>
+$id => undef
+</%args>
+<%init>
+my $t = RT::Ticket->new($session{CurrentUser});
+$t->Load($id);
+my $history = $t->Transactions()->ItemsArrayRef;
+</%init>
+<&| /m/_elements/wrapper, title => $t->Subject &>
+<div class="history">
+<& /m/_elements/ticket_menu, ticket => $t &>
+<&|/Widgets/TitleBox &>
+<ul class="history-list">
+% for my $entry (reverse @$history) {
+<li>
+<span class="age"><% $entry->CreatedObj->AgeAsString() %></span> -
+<& /Elements/ShowUser, User => $entry->CreatorObj &> - 
+<%$entry->BriefDescription%>
+% if ($entry->Type !~ /EmailRecord/) {
+% if ($entry->ContentObj) {
+<div class="txn-content">
+<%$entry->Content%>
+</div>
+%}
+% }
+</li>
+% }
+</ul>
+</&>
+</div>
+</&>
diff --git a/share/html/m/ticket/modify b/share/html/m/ticket/modify
new file mode 100644
index 0000000..e69de29
diff --git a/share/html/m/ticket/reply b/share/html/m/ticket/reply
new file mode 100644
index 0000000..34dc2c2
--- /dev/null
+++ b/share/html/m/ticket/reply
@@ -0,0 +1,171 @@
+<&|/m/_elements/wrapper, title => loc('Update ticket #[_1]', $t->id) &>
+<& /m/_elements/ticket_menu, ticket => $t &>
+<& /Elements/ListActions, actions => \@results &>
+<div class="ticket-reply">
+<&|/Widgets/TitleBox &>
+<form action="reply" id="update"
+    method="post" enctype="multipart/form-data">
+<input type="hidden" class="hidden" name="DefaultStatus" value="<% $DefaultStatus ||''%>" />
+<input type="hidden" class="hidden" name="Action" value="<% $ARGS{Action}||'' %>" />
+
+<div class="entry"><span class="label"><&|/l&>Status</&>:</span>
+<div class="value">
+<& /Elements/SelectStatus, Name=>"Status", DefaultLabel => loc("[_1] (Unchanged)", loc($t->Status)), Default => $ARGS{'Status'} || ($t->Status eq $DefaultStatus ? undef : $DefaultStatus)&>
+</div></div>
+
+<div class="entry"><span class="label"><&|/l&>Owner</&>:</span>
+<div class="value">
+<& /Elements/SelectOwner,
+    Name         => "Owner",
+    TicketObj    => $t,
+    QueueObj     => $t->QueueObj,
+    DefaultLabel => loc("[_1] (Unchanged)", $t->OwnerObj->Name),
+    Default      => $ARGS{'Owner'}
+&>
+</div></div>
+<div class="entry timefield"><span class="label"><&|/l&>Worked</&>:</span><span class="value">
+<& /Elements/EditTimeValue,
+    Name => 'UpdateTimeWorked',
+    Default => $ARGS{UpdateTimeWorked}||'',
+    InUnits => $ARGS{'UpdateTimeWorked-TimeUnits'}||'minutes',
+&>
+</span></div>
+<input type="hidden" class="hidden" name="id" value="<%$t->Id%>" /><br />
+<div class="entry"><span class="label"><&|/l&>Update Type</&>:</span>
+<div class="value"><select name="UpdateType">
+% if ($CanComment) {
+<option value="private" <% ($ARGS{'UpdateType'} &&  $ARGS{'UpdateType'} eq "private") ? qq[ selected="selected"] : !$ARGS{'UpdateType'}&&$CommentDefault |n %>><&|/l&>Comments (Not sent to requestors)</&></option>
+% }
+% if ($CanRespond) {
+<option value="response" <% ($ARGS{'UpdateType'} && $ARGS{'UpdateType'} eq "response") ? qq[ selected="selected"] : !$ARGS{'UpdateType'}&&$ResponseDefault |n %>><&|/l&>Reply to requestors</&></option>
+% }
+</select> 
+</div></div>
+<div class="entry"><span class="label"><&|/l&>Subject</&>:</span><div class="value"> <input name="UpdateSubject" size="60" value="<% $ARGS{UpdateSubject} || $t->Subject()%>" />
+% $m->callback( %ARGS, CallbackName => 'AfterSubject' );
+</div></div>
+
+<div class="entry"><span class="label"><&|/l&>One-time Cc</&>:</span><span class="value"><& /Elements/EmailInput, Name => 'UpdateCc', Size => '60', Default => $ARGS{UpdateCc} &></span></div>
+
+<div class="entry"><span class="label"><&|/l&>One-time Bcc</&>:</span><span class="value"><& /Elements/EmailInput, Name => 'UpdateBcc', Size => '60', Default => $ARGS{UpdateBcc} &></span></div>
+
+<div class="entry"><span class="label" ><&|/l&>Message</&>:</span><div class="value">
+% if (exists $ARGS{UpdateContent}) {
+% # preserve QuoteTransaction so we can use it to set up sane references/in/reply to
+% my $temp = $ARGS{'QuoteTransaction'};
+% delete $ARGS{'QuoteTransaction'};
+<& /Elements/MessageBox, Name=>"UpdateContent", Default=>$ARGS{UpdateContent}, IncludeSignature => 0, %ARGS&>
+% $ARGS{'QuoteTransaction'} = $temp;
+% } else {
+% my $IncludeSignature = 1;
+% $IncludeSignature = 0 if $Action ne 'Respond' && !RT->Config->Get('MessageBoxIncludeSignatureOnComment');
+<& /Elements/MessageBox, Name=>"UpdateContent", IncludeSignature => $IncludeSignature, %ARGS &>
+% }
+</div></div>
+<& /Elements/Submit, Label => loc('Update Ticket'), Name => 'SubmitTicket' &>
+</form>
+</&>
+</div>
+</&>
+<%INIT>
+my $CanRespond = 0;
+my $CanComment = 0;
+my $checks_failure = 0;
+my $title;
+
+my $t = LoadTicket($id);
+
+my @results;
+
+$m->callback( Ticket => $t, ARGSRef => \%ARGS, results => \@results, CallbackName => 'Initial' );
+
+unless($DefaultStatus){
+    $DefaultStatus=($ARGS{'Status'} ||$t->Status());
+}
+
+if ($DefaultStatus eq 'new'){
+    $DefaultStatus='open';
+}
+
+if ($DefaultStatus eq 'resolved') {
+    $title = loc("Resolve ticket #[_1] ([_2])", $t->id, $t->Subject);
+} else {
+    $title = loc("Update ticket #[_1] ([_2])", $t->id, $t->Subject);
+}
+
+# Things needed in the template - we'll do the processing here, just
+# for the convenience:
+
+my ($CommentDefault, $ResponseDefault);
+if ($Action ne 'Respond') {
+    $CommentDefault = qq[ selected="selected"]; 
+    $ResponseDefault = "";
+} else {
+    $CommentDefault = ""; 
+    $ResponseDefault = qq[ selected="selected"];
+}
+
+
+
+$CanRespond = 1 if ( $t->CurrentUserHasRight('ReplyToTicket') or
+                     $t->CurrentUserHasRight('ModifyTicket') ); 
+
+$CanComment = 1 if ( $t->CurrentUserHasRight('CommentOnTicket') or
+                     $t->CurrentUserHasRight('ModifyTicket') ); 
+
+
+# {{{ deal with deleting uploaded attachments
+foreach my $key (keys %ARGS) {
+    if ($key =~ m/^DeleteAttach-(.+)$/) {
+        delete $session{'Attachments'}{$1};
+    }
+    $session{'Attachments'} = { %{$session{'Attachments'} || {}} };
+}
+# }}}
+
+# {{{ store the uploaded attachment in session
+if ($ARGS{'Attach'}) {            # attachment?
+    my $attachment = MakeMIMEEntity(
+        AttachmentFieldName => 'Attach'
+    );
+
+    my $file_path = Encode::decode_utf8("$ARGS{'Attach'}");
+    $session{'Attachments'} = {
+        %{$session{'Attachments'} || {}},
+        $file_path => $attachment,
+    };
+}
+# }}}
+
+# delete temporary storage entry to make WebUI clean
+unless (keys %{$session{'Attachments'}} and $ARGS{'UpdateAttach'}) {
+    delete $session{'Attachments'};
+}
+# }}}
+
+# check email addresses for RT's
+{
+    foreach my $field ( qw(UpdateCc UpdateBcc) ) {
+        my $value = $ARGS{ $field };
+        next unless defined $value && length $value;
+
+        my @emails = Email::Address->parse( $value );
+        foreach my $email ( grep RT::EmailParser->IsRTAddress($_->address), @emails ) {
+            push @results, loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email->format, loc(substr($field, 6)) );
+            $checks_failure = 1;
+            $email = undef;
+        }
+        $ARGS{ $field } = join ', ', map $_->format, grep defined, @emails;
+    }
+}
+
+if ( !$checks_failure && exists $ARGS{SubmitTicket} ) {
+    return $m->comp('/m/ticket/show', TicketObj => $t, %ARGS);
+}
+</%INIT>
+
+<%ARGS>
+$id => undef
+$Action => undef
+$DefaultStatus => undef
+</%ARGS>
diff --git a/share/html/m/ticket/select_create_queue b/share/html/m/ticket/select_create_queue
new file mode 100644
index 0000000..88cf203
--- /dev/null
+++ b/share/html/m/ticket/select_create_queue
@@ -0,0 +1,18 @@
+<%init>
+my $queues = RT::Queues->new($session{'CurrentUser'});
+$queues->UnLimit();
+
+</%init>
+<&| /m/_elements/wrapper, title => loc("Create a ticket") &>
+<div class="select_queue">
+<&|/Widgets/TitleBox, title => loc("Select a queue") &>
+<ul class="menu">
+% while (my $q = $queues->Next()) {
+% next if (! $q->CurrentUserHasRight('CreateTicket'));
+<li><a href="<%RT->Config->Get('WebPath')%>/m/ticket/create?Queue=<%$q->id%>"><%$q->Name%></a></li>
+% }
+</ul>
+</&>
+</div>
+</&>
+
diff --git a/share/html/m/ticket/show b/share/html/m/ticket/show
new file mode 100644
index 0000000..c650019
--- /dev/null
+++ b/share/html/m/ticket/show
@@ -0,0 +1,431 @@
+<%args>
+$id => undef
+</%args>
+<%init>
+my $Ticket;
+my @Actions; 
+
+if ($ARGS{'id'} eq 'new') {
+    # {{{ Create a new ticket
+
+    my $Queue = new RT::Queue( $session{'CurrentUser'} );
+    $Queue->Load($ARGS{'Queue'});
+    unless ( $Queue->id ) {
+        Abort('Queue not found');
+    }
+
+    unless ( $Queue->CurrentUserHasRight('CreateTicket') ) {
+        Abort('You have no permission to create tickets in that queue.');
+    }
+
+    ($Ticket, @Actions) = CreateTicket(
+        Attachments => delete $session{'Attachments'},
+        %ARGS,
+    );
+    unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
+        Abort("No permission to view newly created ticket #".$Ticket->id.".");
+    }
+    # }}}
+} else { 
+    $Ticket ||= LoadTicket($ARGS{'id'});
+
+    $m->callback( CallbackName => 'BeforeProcessArguments',
+        TicketObj => $Ticket,
+        ActionsRef => \@Actions, ARGSRef => \%ARGS );
+    if ( defined $ARGS{'Action'} ) {
+        if ($ARGS{'Action'} =~ /^(Steal|Kill|Take|SetTold)$/) {
+            my $action = $1;
+            my ($res, $msg) = $Ticket->$action();
+            push(@Actions, $msg);
+        }
+    }
+
+    $m->callback(CallbackName => 'ProcessArguments', 
+            Ticket => $Ticket, 
+            ARGSRef => \%ARGS, 
+            Actions => \@Actions);
+    
+    $ARGS{UpdateAttachments} = $session{'Attachments'};
+    push @Actions,
+        ProcessUpdateMessage(
+        ARGSRef   => \%ARGS,
+        Actions   => \@Actions,
+        TicketObj => $Ticket,
+        );
+    delete $session{'Attachments'};
+
+    #Process status updates
+    push @Actions, ProcessTicketWatchers(ARGSRef => \%ARGS, TicketObj => $Ticket );
+    push @Actions, ProcessTicketBasics(  ARGSRef => \%ARGS, TicketObj => $Ticket );
+    push @Actions, ProcessTicketLinks(   ARGSRef => \%ARGS, TicketObj => $Ticket );
+    push @Actions, ProcessTicketDates(   ARGSRef => \%ARGS, TicketObj => $Ticket );
+    push @Actions, ProcessObjectCustomFieldUpdates(ARGSRef => \%ARGS, TicketObj => $Ticket );
+
+    # XXX: we shouldn't block actions here if user has no right to see the ticket,
+    # but we should allow him to see actions he has done
+    unless ($Ticket->CurrentUserHasRight('ShowTicket')) {
+        Abort("No permission to view ticket");
+    }
+    if ( $ARGS{'MarkAsSeen'} ) {
+        $Ticket->SetAttribute(
+            Name => 'User-'. $Ticket->CurrentUser->id .'-SeenUpTo',
+            Content => $Ticket->LastUpdated,
+        );
+        push @Actions, loc('Marked all messages as seen');
+    }
+}
+
+$m->callback(
+    CallbackName => 'BeforeDisplay',
+    TicketObj => \$Ticket,
+    Actions => \@Actions,
+    ARGSRef => \%ARGS,
+);
+
+# This code does automatic redirection if any updates happen. 
+
+if (@Actions) {
+
+    # We've done something, so we need to clear the decks to avoid
+    # resubmission on refresh.
+    # But we need to store Actions somewhere too, so we don't lose them.
+    my $key = Digest::MD5::md5_hex( rand(1024) );
+    push @{ $session{"Actions"}->{$key} ||= [] }, @Actions;
+    $session{'i'}++;
+    my $url = RT->Config->Get('WebURL') . "/m/ticket/show?id=" . $Ticket->id . "&results=" . $key;
+    $url .= '#' . $ARGS{Anchor} if $ARGS{Anchor};
+    RT::Interface::Web::Redirect($url);
+}
+
+# If we haven't been passed in an Attachments object (through the precaching mechanism)
+# then we need to find one
+my $Attachments = $m->comp('/Ticket/Elements/FindAttachments', Ticket => $Ticket);
+
+my %documents;
+while ( my $attach = $Attachments->Next() ) {
+    next unless ($attach->Filename());
+   unshift( @{ $documents{ $attach->Filename } }, $attach );
+}
+
+my $CustomFields = $Ticket->CustomFields;
+$m->callback(
+    CallbackName => 'MassageCustomFields',
+    Object => $Ticket,
+    CustomFields => $CustomFields,
+);
+
+my $print_value = sub {
+    my ($cf, $value) = @_;
+    my $linked = $value->LinkValueTo;
+    if ( defined $linked && length $linked ) {
+        my $linked = $m->interp->apply_escapes( $linked, 'h' );
+        $m->out('<a href="'. $linked .'" target="_new">');
+    }
+    my $comp = "ShowCustomField". $cf->Type;
+    $m->callback(
+        CallbackName => 'ShowComponentName',
+        Name         => \$comp,
+        CustomField  => $cf,
+        Object       => $Ticket,
+    );
+    if ( $m->comp_exists( $comp ) ) {
+        $m->comp( $comp, Object => $value );
+    } else {
+        $m->out( $m->interp->apply_escapes( $value->Content, 'h' ) );
+    }
+    $m->out('</a>') if defined $linked && length $linked;
+
+    # This section automatically populates a<div with the "IncludeContentForValue" for this custom
+    # field if it's been defined
+    if ( $cf->IncludeContentForValue ) {
+       my $vid = $value->id;
+       $m->out(   '<div class="object_cf_value_include" id="object_cf_value_'. $vid .'">' );
+       $m->print( loc("See also:") );
+       $m->out(   '<a href="'. $value->IncludeContentForValue .'">' );
+       $m->print( $value->IncludeContentForValue );
+       $m->out(   qq{</a></div>\n} );
+       $m->out(   qq{<script><!--\nahah('} );
+       $m->print( $value->IncludeContentForValue );
+       $m->out(   qq{', 'object_cf_value_$vid');\n--></script>\n} );
+    }
+};
+
+</%init>
+<&| /m/_elements/wrapper, title => $Ticket->Subject &>
+<div id="ticket-show">
+<& /m/_elements/ticket_menu, ticket => $Ticket &>
+
+    <&| /Widgets/TitleBox, title => loc('The Basics'),
+        class => 'ticket-info-basics',
+    &>
+
+
+ <div class="entry">
+    <div class="label id"><&|/l&>Id</&>:</div>
+    <div class="value id"><%$Ticket->Id %></div>
+  </div>
+ <div class="entry">
+    <div class="label status"><&|/l&>Status</&>:</div>
+    <div class="value status"><% loc($Ticket->Status) %></div>
+  </div>
+% if ($Ticket->TimeEstimated) {
+ <div class="entry">
+    <div class="label time estimated"><&|/l&>Estimated</&>:</div>
+    <div class="value time estimated"><& /Ticket/Elements/ShowTime, minutes => $Ticket->TimeEstimated &></div>
+  </div>
+% }
+% if ($Ticket->TimeWorked) {
+ <div class="entry">
+    <div class="label time worked"><&|/l&>Worked</&>:</div>
+    <div class="value time worked"><& /Ticket/Elements/ShowTime, minutes => $Ticket->TimeWorked &></div>
+  </div>
+% }
+% if ($Ticket->TimeLeft) {
+ <div class="entry">
+    <div class="label time left"><&|/l&>Left</&>:</div>
+    <div class="value time left"><& /Ticket/Elements/ShowTime, minutes => $Ticket->TimeLeft &></div>
+  </div>
+% }
+ <div class="entry">
+    <div class="label priority"><&|/l&>Priority</&>:</div>
+    <div class="value priority"><& /Ticket/Elements/ShowPriority, Ticket => $Ticket &></div>
+  </div>
+ <div class="entry">
+    <div class="label queue"><&|/l&>Queue</&>:</div>
+    <div class="value queue"><& /Ticket/Elements/ShowQueue, QueueObj => $Ticket->QueueObj &></div>
+  </div>
+    </&>
+
+% if ($Ticket->CustomFields->First) {
+    <&| /Widgets/TitleBox, title => loc('Custom Fields'),
+        class => 'ticket-info-cfs',
+    &>
+
+% while ( my $CustomField = $CustomFields->Next ) {
+% my $Values = $Ticket->CustomFieldValues( $CustomField->Id );
+% my $count = $Values->Count;
+  <div class="entry" id="CF-<%$CustomField->id%>-ShowRow">
+    <div class="label"><% $CustomField->Name %>:</div>
+    <div class="value">
+% unless ( $count ) {
+<i><&|/l&>(no value)</&></i>
+% } elsif ( $count == 1 ) {
+%   $print_value->( $CustomField, $Values->First );
+% } else {
+<ul>
+% while ( my $Value = $Values->Next ) {
+<li>
+% $print_value->( $CustomField, $Value );
+</li>
+% }
+</ul>
+% }
+    </div>
+  </div>
+% }
+
+</&>
+% }
+
+    <&| /Widgets/TitleBox, title => loc('People'), class => 'ticket-info-people' &>
+
+
+ <div class="entry">
+    <div class="label"><&|/l&>Owner</&>:</div>
+    <div class="value"><& /Elements/ShowUser, User => $Ticket->OwnerObj, Ticket => $Ticket &>
+    </div>
+  </div>
+ <div class="entry">
+    <div class="label"><&|/l&>Requestors</&>:</div>
+    <div class="value"><& /Ticket/Elements/ShowGroupMembers, Group => $Ticket->Requestors, Ticket => $Ticket &></div>
+  </div>
+ <div class="entry">
+    <div class="label"><&|/l&>Cc</&>:</div>
+    <div class="value"><& /Ticket/Elements/ShowGroupMembers, Group => $Ticket->Cc, Ticket => $Ticket &></div>
+  </div>
+ <div class="entry">
+    <div class="label"><&|/l&>AdminCc</&>:</div>
+    <div class="value"><& /Ticket/Elements/ShowGroupMembers, Group => $Ticket->AdminCc, Ticket => $Ticket &></div>
+  </div>
+
+    </&>
+
+% if (keys %documents) {
+<&| /Widgets/TitleBox, title => loc('Attachments'), 
+        title_class=> 'inverse',  
+        class => 'ticket-info-attachments',
+        color => "#336699" &>
+
+% foreach my $key (keys %documents) {
+
+<%$key%><br />
+<ul>
+% foreach my $rev (@{$documents{$key}}) {
+
+<%PERL>
+my $size = $rev->ContentLength;
+
+if ($size) {
+    my $kb = int($size/102.4) / 10;
+    my $units = RT->Config->Get('AttachmentUnits');
+
+    if (!defined($units)) {
+        if ($size > 1024) {
+            $size = $kb . "k";
+        }
+        else {
+            $size = $size . "b";
+        }
+    }
+    elsif ($units eq 'k') {
+        $size = $kb . "k";
+    }
+    else {
+        $size = $size . "b";
+    }
+
+</%PERL>
+
+<li><font size="-2">
+<a href="<%RT->Config->Get('WebPath')%>/Ticket/Attachment/<%$rev->TransactionId%>/<%$rev->Id%>/<%$rev->Filename | u%>">
+<&|/l, $rev->CreatedAsString, $size, $rev->CreatorObj->Name &>[_1] ([_2]) by [_3]</&>
+</a>
+</font></li>
+% }
+% }
+</ul>
+
+% }
+</&>
+
+% }
+% # too painful to deal with reminders
+% if ( 0 &&  RT->Config->Get('EnableReminders') ) {
+    <&|/Widgets/TitleBox, title => loc("Reminders"),
+        class => 'ticket-info-reminders',
+    &>
+       <div class="entry"><div
+            <form action="<%RT->Config->Get('WebPath')%>/Ticket/Display.html" method="post">
+                <& /Ticket/Elements/Reminders, Ticket => $Ticket, ShowCompleted => 0 &>
+                <div align="right"><input type="submit" class="button" value="<&|/l&>Save</&>" /></div>
+            </form>
+        </div></div>
+    </&>
+% }
+
+    <&| /Widgets/TitleBox, title => loc("Dates"),
+        class => 'ticket-info-dates',
+    &>
+
+
+ <div class="entry">
+    <div class="label date created"><&|/l&>Created</&>:</div>
+    <div class="value date created"><% $Ticket->CreatedObj->AsString %></div>
+  </div>
+ <div class="entry">
+    <div class="label date starts"><&|/l&>Starts</&>:</div>
+    <div class="value date starts"><% $Ticket->StartsObj->AsString %></div>
+  </div>
+ <div class="entry">
+    <div class="label date started"><&|/l&>Started</&>:</div>
+    <div class="value date started"><% $Ticket->StartedObj->AsString %></div>
+  </div>
+ <div class="entry">
+    <div class="label date told"><&|/l&>Last Contact</&>:</div>
+    <div class="value date told"><% $Ticket->ToldObj->AsString %></div>
+  </div>
+ <div class="entry">
+    <div class="label date due"><&|/l&>Due</&>:</div>
+% my $due = $Ticket->DueObj;
+% if ( $due && $due->Unix > 0 && $due->Diff < 0 ) {
+    <div class="value date due"><span class="overdue"><% $due->AsString  %></span></div>
+% } else {
+    <div class="value date due"><% $due->AsString  %></div>
+% }
+  </div>
+ <div class="entry">
+    <div class="label date resolved"><&|/l&>Closed</&>:</div>
+    <div class="value date resolved"><% $Ticket->ResolvedObj->AsString  %></div>
+  </div>
+ <div class="entry">
+    <div class="label date updated"><&|/l&>Updated</&>:</div>
+% my $UpdatedString = $Ticket->LastUpdated ? loc("[_1] by [_2]", $Ticket->LastUpdatedAsString, $Ticket->LastUpdatedByObj->Name) : loc("Never");
+    <div class="value date updated"><% $UpdatedString | h %></div>
+  </div>
+
+    </&>
+
+    <&| /Widgets/TitleBox, title => loc('Links'), class => 'ticket-info-links' &>
+
+ <div class="entry">
+    <div class="label"><% loc('Depends on')%>:</div>
+    <div class="value">
+
+<%PERL>
+my ( @active, @inactive, @not_tickets );
+for my $link ( @{ $Ticket->DependsOn->ItemsArrayRef } ) {
+    my $target = $link->TargetObj;
+    if ( $target && $target->isa('RT::Ticket') ) {
+        if ( $target->QueueObj->IsInactiveStatus( $target->Status ) ) {
+            push( @inactive, $link->TargetURI );
+        }
+        else {
+            push( @active, $link->TargetURI );
+        }
+    }
+    else {
+        push( @not_tickets, $link->TargetURI );
+    }
+}
+</%PERL>
+
+
+<ul>
+% for my $Link (@not_tickets, @active, @inactive) {
+<li><& /Elements/ShowLink, URI => $Link &></li>
+% }
+</ul>
+    </div>
+  </div>
+ <div class="entry">
+    <div class="label"><% loc('Depended on by')%>:</div>
+    <div class="value">
+<ul>
+% while (my $Link = $Ticket->DependedOnBy->Next) {
+<li><& /Elements/ShowLink, URI => $Link->BaseURI &></li>
+% }
+</ul>
+    </div>
+  </div>
+ <div class="entry">
+    <div class="label"><% loc('Parents') %>:</div>
+    <div class="value"><& /Ticket/Elements/ShowParents, Ticket => $Ticket &></div>
+  </div>
+ <div class="entry">
+    <div class="label"><% loc('Children')%>:</div>
+    <div class="value"><& /Ticket/Elements/ShowMembers, Ticket => $Ticket &></div>
+  </div>
+ <div class="entry">
+    <div class="label"><% loc('Refers to')%>:</div>
+    <div class="value">
+<ul>
+% while (my $Link = $Ticket->RefersTo->Next) {
+<li><& /Elements/ShowLink, URI => $Link->TargetURI &></li>
+% }
+</ul>
+    </div>
+  </div>
+ <div class="entry">
+    <div class="label"><% loc('Referred to by')%>:</div>
+    <div class="value">
+    <ul>
+% while (my $Link = $Ticket->ReferredToBy->Next) {
+% next if (UNIVERSAL::isa($Link->BaseObj, 'RT::Ticket')  && $Link->BaseObj->Type eq 'reminder');
+<li><& /Elements/ShowLink, URI => $Link->BaseURI &></li>
+% }
+</ul>
+    </div>
+  </div>
+    </&>
+</div>
+</&>
diff --git a/share/html/m/tickets/requested b/share/html/m/tickets/requested
new file mode 100644
index 0000000..3043e05
--- /dev/null
+++ b/share/html/m/tickets/requested
@@ -0,0 +1,4 @@
+<%init>
+ $m->comp('../_elements/ticket_list', %ARGS, query => 'Requestors.EmailAddress = "'.$session{CurrentUser}->EmailAddress.'" AND (Status != "resolved" AND Status != "rejected" AND Status != "stalled")'); 
+$m->abort();
+</%init>
diff --git a/share/html/m/tickets/search b/share/html/m/tickets/search
new file mode 100644
index 0000000..16864b4
--- /dev/null
+++ b/share/html/m/tickets/search
@@ -0,0 +1,64 @@
+<%args>
+$page => 1
+$order_by => 'id'
+$order => 'desc'
+$name => undef
+</%args>
+<%init>
+use RT::Search::Googleish;
+my $query = $ARGS{'query'};
+if ($ARGS{'q'}) {
+    my $tickets = RT::Tickets->new( $session{'CurrentUser'} );
+    my %args = (
+        Argument   => $ARGS{q},
+        TicketsObj => $tickets,
+    );
+    my $search = RT::Search::Googleish->new(%args);
+    $query = $search->QueryToSQL();
+
+}
+
+elsif ($ARGS{'name'}) {
+my $search_arg;
+
+my $search;
+
+    if ($name)  {
+        ($search) = RT::System->new( $session{'CurrentUser'} )->Attributes->Named( 'Search - ' . $name );
+        unless ( $search && $search->Id ) {
+            my (@custom_searches) = RT::System->new( $session{'CurrentUser'} )->Attributes->Named('SavedSearch');
+            foreach my $custom (@custom_searches) {
+                if ( $custom->Description eq $name ) { $search = $custom; last }
+            }
+            unless ( $search && $search->id ) {
+                $m->out("Predefined search $name not found");
+                return;
+            }
+        }
+
+        $search_arg = $session{'CurrentUser'}->UserObj->Preferences( $search, $search->Content );
+    }
+
+    foreach ($search_arg) {
+        if ( $_->{'Query'} =~ /__Bookmarks__/ ) {
+            $_->{'Rows'} = 999;
+
+            # DEPRECATED: will be here for a while up to 3.10/4.0
+            my $bookmarks = $session{'CurrentUser'}->UserObj->FirstAttribute('Bookmarks');
+            $bookmarks = $bookmarks->Content if $bookmarks;
+            $bookmarks ||= {};
+            my $query = join( " OR ", map " id = '$_' ", grep $bookmarks->{$_}, keys %$bookmarks ) || 'id=0';
+            $_->{'Query'} =~ s/__Bookmarks__/( $query )/g;
+        }
+    }
+
+    $query    = $search_arg->{Query};
+    $order_by = $search_arg->{OrderBy};
+    $order    = $search_arg->{Order};
+
+}
+
+
+$m->comp('../_elements/ticket_list', query => $query, page => $page, order_by => $order_by, order => $order);
+$m->abort();
+</%init>

commit fac7bab8953faaf718df860dc0f72961cea13f3b
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Tue Aug 17 20:10:55 2010 +0800

    basic dynamic css apply test.

diff --git a/share/html/Admin/Elements/SystemTabs b/share/html/Admin/Elements/SystemTabs
index cb51fb6..bbf4271 100755
--- a/share/html/Admin/Elements/SystemTabs
+++ b/share/html/Admin/Elements/SystemTabs
@@ -73,6 +73,9 @@
                 I => { title => loc('RT at a glance'),
                                 path => 'Admin/Global/MyRT.html',
                       },
+                J => { title => loc('Theme'),
+                                path => 'Admin/Global/Theme.html',
+                      },
 
 };
 
diff --git a/share/html/Admin/Elements/SystemTabs b/share/html/Admin/Global/Theme.html
old mode 100755
new mode 100644
similarity index 58%
copy from share/html/Admin/Elements/SystemTabs
copy to share/html/Admin/Global/Theme.html
index cb51fb6..7ec00aa
--- a/share/html/Admin/Elements/SystemTabs
+++ b/share/html/Admin/Global/Theme.html
@@ -45,55 +45,41 @@
 %# those contributions and any derivatives thereof.
 %# 
 %# END BPS TAGGED BLOCK }}}
-<& /Admin/Elements/Tabs, subtabs => $tabs, 
-    current_tab => 'Admin/Global/', 
-    current_subtab => $current_tab, 
-    Title => $Title &>
+<& /Admin/Elements/Header, Title => loc("Theme") &>
+<& /Admin/Elements/SystemTabs, 
+    current_tab => 'Admin/Global/Theme.html', 
+    Title => loc("Theme") &>
+<& /Elements/ListActions, actions => \@results &>
 
-<%INIT>
-  my $tabs = {
-                
-               A => { title => loc('Scrips'),
-                           path => 'Admin/Global/Scrips.html',
-                         },
-               B => { title => loc('Templates'),
-                        path => 'Admin/Global/Templates.html',
-                      },
-              
-                F => { title => loc('Custom Fields'),
-                        path => 'Admin/Global/CustomFields/index.html',
-                        },
+<style type="text/css" media="screen" id="test">
+</style>
 
-                G => { title => loc('Group Rights'),
-                                path => 'Admin/Global/GroupRights.html',
-                      },
-                H => { title => loc('User Rights'),
-                                path => 'Admin/Global/UserRights.html',
-                      },
-                I => { title => loc('RT at a glance'),
-                                path => 'Admin/Global/MyRT.html',
-                      },
 
-};
+Csutom CSS:
+<textarea rows=30 cols=60 id="user_css">
+body {background: orange}
 
-  # Now let callbacks add their extra tabs
-  $m->callback( %ARGS, tabs => $tabs );
+div#header h1 { color:black }
 
-  if ($current_tab) {
-    foreach my $tab (sort keys %{$tabs}) {
-      if ($tabs->{$tab}->{'path'} eq $current_tab) {
-        $tabs->{$tab}->{"subtabs"} = $subtabs || {};
-        $tabs->{$tab}->{"current_subtab"} = $current_subtab;
-      }
-    }
-  }
-</%INIT>
+input[type="reset"], input[type="submit"], input[class="button"] {background: green}
+
+</textarea>
+
+<input id="try" type="button" value="Try">
+
+<script type="text/javascript">
+jQuery(function($) {
+  $('#try').click(function() {
+    $("style#test").text($('#user_css').val());
+  })
 
-  
+});
+</script>
+
+<%INIT>
+my @results;
+
+</%INIT>
 <%ARGS>
-$id => undef
-$current_tab => undef
-$subtabs => undef
-$current_subtab => undef
-$Title => undef
+
 </%ARGS>

commit ba000a2946974cce957498aab0129b97bc99131f
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Wed Aug 18 23:51:23 2010 +0800

    primary color analyzer.

diff --git a/share/html/Admin/Global/Theme.html b/share/html/Admin/Global/Theme.html
index 7ec00aa..de5604c 100644
--- a/share/html/Admin/Global/Theme.html
+++ b/share/html/Admin/Global/Theme.html
@@ -54,6 +54,21 @@
 <style type="text/css" media="screen" id="test">
 </style>
 
+Upload Logo:
+<form method="POST" enctype="multipart/form-data">
+<input type="file" name="logo-upload" />
+<input type="submit">
+</form>
+
+% if ($colors) {
+Primary colors:
+%   for (@$colors) {
+%     my $fg = $_->{l} >= 0.6 ? 'black' : 'white';
+<span style="background-color: rgb(<% $_->{c} %>); color: <% $fg %>; width: 5em">test l=<% $_->{l}%>></span>
+%   }
+% }
+
+<div class="clear">
 
 Csutom CSS:
 <textarea rows=30 cols=60 id="user_css">
@@ -79,6 +94,51 @@ jQuery(function($) {
 <%INIT>
 my @results;
 
+use Imager;
+use Graphics::Color::RGB;
+
+my $img = Imager->new;
+if (my $file_hash = _UploadedFile( 'logo-upload' )) {
+    my ($id, $msg) = $RT::System->SetAttribute( Name => "UserLogo", Description => "User-provided logo", Content => \$file_hash->{LargeContent} );
+    $img->read(data => $file_hash->{LargeContent} );
+}
+else {
+    my $attr = $RT::System->FirstAttribute('UserLogo');
+    my $content = $attr->Content;
+    $img->read(data => $$content) or die "Cannot read: ", $img->errstr;
+}
+
+my $colors = $img ? analyze_img($img) : undef;
+use List::MoreUtils qw(uniq);
+
+sub analyze_img {
+    my $img = shift;
+    my $color;
+
+    for my $i (0..$img->getwidth-1) {
+        for my $j (0..$img->getheight-1) {
+            my @color = $img->getpixel(x=>$i, y=>$j)->rgba;
+            my $hsl = Graphics::Color::RGB->new( red => $color[0] / 255, green => $color[2] / 255, blue => $color[2] / 255, alpha => $color[3] / 255 )->to_hsl;
+            my ($h,$s,$l) = $hsl->as_array;
+            pop @color;
+            my $c = join(',', at color);
+            next if $l < 0.1;
+            $color->{$c} ||= { h => $h, s => $s, l => $l, cnt => 0, c => $c};
+            $color->{$c}->{cnt}++;
+        }
+    }
+
+    for (values %$color) {
+        $_->{rank} = $_->{s} * $_->{cnt};
+    }
+    my @top5 = (sort { $b->{rank} <=> $a->{rank} } values %$color)[0..4];
+    if ((scalar uniq map {$_->{rank}} @top5) == 1) {
+        warn "bad";
+    }
+    return \@top5;
+}
+
+
 </%INIT>
 <%ARGS>
 

commit 8298628a4d8e9f0860ee99a93ee3d7b08167b78d
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Wed Aug 25 00:23:54 2010 +0800

    store content-type and helper to show user logo.

diff --git a/share/html/Admin/Global/Theme.html b/share/html/Admin/Global/Theme.html
index de5604c..05b5852 100644
--- a/share/html/Admin/Global/Theme.html
+++ b/share/html/Admin/Global/Theme.html
@@ -55,6 +55,9 @@
 </style>
 
 Upload Logo:
+% if ($img) {
+<img src="/Helpers/UserLogo?<% time() %>" />
+% }
 <form method="POST" enctype="multipart/form-data">
 <input type="file" name="logo-upload" />
 <input type="submit">
@@ -99,13 +102,22 @@ use Graphics::Color::RGB;
 
 my $img = Imager->new;
 if (my $file_hash = _UploadedFile( 'logo-upload' )) {
-    my ($id, $msg) = $RT::System->SetAttribute( Name => "UserLogo", Description => "User-provided logo", Content => \$file_hash->{LargeContent} );
+    my ($id, $msg) = $RT::System->SetAttribute( Name => "UserLogo",
+                                                Description => "User-provided logo",
+                                                Content => {
+                                                    type => $file_hash->{ContentType},
+                                                    data => $file_hash->{LargeContent} } );
     $img->read(data => $file_hash->{LargeContent} );
 }
 else {
     my $attr = $RT::System->FirstAttribute('UserLogo');
     my $content = $attr->Content;
-    $img->read(data => $$content) or die "Cannot read: ", $img->errstr;
+    if (ref($content) eq 'HASH') {
+        $img->read(data => $content->{data}) or die "Cannot read: ", $img->errstr;
+    }
+    else {
+        $RT::System->DeleteAttribute('UserLogo');
+    }
 }
 
 my $colors = $img ? analyze_img($img) : undef;
diff --git a/share/html/Helpers/UserLogo b/share/html/Helpers/UserLogo
new file mode 100644
index 0000000..6cf1735
--- /dev/null
+++ b/share/html/Helpers/UserLogo
@@ -0,0 +1,11 @@
+<%INIT>
+RT::Interface::Web::StaticFileHeaders();
+if ( my $attr = $RT::System->FirstAttribute('UserLogo') ) {
+    my $content = $attr->Content;
+    $r->content_type($content->{type}) ;
+    $m->out( $content->{data} );
+    $m->abort;
+}
+
+
+</%INIT>

commit 5913433a78d2159b3c012da29a5d8994f35f3850
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Wed Aug 25 01:07:30 2010 +0800

    more style application and save

diff --git a/share/html/Admin/Global/Theme.html b/share/html/Admin/Global/Theme.html
index 05b5852..ad4a4c6 100644
--- a/share/html/Admin/Global/Theme.html
+++ b/share/html/Admin/Global/Theme.html
@@ -67,29 +67,61 @@ Upload Logo:
 Primary colors:
 %   for (@$colors) {
 %     my $fg = $_->{l} >= 0.6 ? 'black' : 'white';
-<span style="background-color: rgb(<% $_->{c} %>); color: <% $fg %>; width: 5em">test l=<% $_->{l}%>></span>
+<span class="color-template" style="background-color: rgb(<% $_->{c} %>); color: <% $fg %>; width: 5em">test l=<% $_->{l}%>></span>
 %   }
 % }
-
+<div class="clear">
+<span class="color-template" style="background-color: white; color: black; width: 5em">test</span>
+<span class="color-template" style="background-color: white; color: gray; width: 5em">test</span>
+<span class="color-template" style="background-color: red; color: white; width: 5em">test</span>
+</div>
 <div class="clear">
 
-Csutom CSS:
-<textarea rows=30 cols=60 id="user_css">
-body {background: orange}
-
-div#header h1 { color:black }
+Csutom CSS (Advanced):<br />
 
-input[type="reset"], input[type="submit"], input[class="button"] {background: green}
+<form method="POST">
 
-</textarea>
+<textarea rows=30 cols=60 id="user_css" name="user_css"><% $user_css %></textarea>
 
 <input id="try" type="button" value="Try">
+<input name="reset_css" value="Reset CSS" type="submit">
+<input value="Save" type="submit">
+</form>
 
 <script type="text/javascript">
+
+var section = 'main';
+
+var section_css_mapping = {
+    main: ['body',
+           'div#body',
+           'div#quickbar',
+           'input[type="reset"], input[type="submit"], input[class="button"]']
+};
+
 jQuery(function($) {
-  $('#try').click(function() {
     $("style#test").text($('#user_css').val());
-  })
+    $('#try').click(function() {
+        $("style#test").text($('#user_css').val());
+    });
+
+
+    $('span.color-template').click(function() {
+      var bg = $(this).css('background-color');
+      var fg = $(this).css('color');
+      var applying = section_css_mapping[section];
+      var css = $('#user_css').val();
+      if (applying) {
+          for (var name in applying) {
+              var rule = new RegExp('\\b'+applying[name]+'\\s*{.*?}');
+              css = css.replace(rule, applying[name]+' { background: '+bg+'; color: '+fg+ '} ');
+          }
+      }
+      $('#user_css').val(css);
+      $("style#test").text(css);
+
+  });
+
 
 });
 </script>
@@ -120,6 +152,33 @@ else {
     }
 }
 
+if ($user_css) {
+    if ($ARGS{'reset_css'}) {
+        $RT::System->DeleteAttribute('UserCSS');
+        undef $user_css;
+    }
+    else {
+        my ($id, $msg) = $RT::System->SetAttribute( Name => "UserCSS",
+                                                    Description => "User-provided css",
+                                                    Content => $user_css );
+    }
+}
+
+if (!$user_css) {
+    my $attr = $RT::System->FirstAttribute('UserCSS');
+    $user_css = $attr ? $attr->Content : 'body {}
+
+div#body {}
+
+div#header h1 { }
+
+div#quickbar {}
+
+input[type="reset"], input[type="submit"], input[class="button"] {}
+';
+
+}
+
 my $colors = $img ? analyze_img($img) : undef;
 use List::MoreUtils qw(uniq);
 
@@ -153,5 +212,5 @@ sub analyze_img {
 
 </%INIT>
 <%ARGS>
-
+$user_css => ''
 </%ARGS>
diff --git a/share/html/Elements/Header b/share/html/Elements/Header
index 9a9f132..ed1b95f 100755
--- a/share/html/Elements/Header
+++ b/share/html/Elements/Header
@@ -75,6 +75,12 @@
 % if ($m->comp_exists($stylesheet_plugin) ) {
 <& $stylesheet_plugin &>
 % }
+% if (my $attr = $RT::System->FirstAttribute('UserCSS')) {
+<style type="text/css" media="screen">
+<% $attr->Content %>
+</style>
+% }
+
 % $m->callback( %ARGS, CallbackName => 'Head' );
 
 </head>

commit 78850128f7b26807b41efdab8972304e7e8587dd
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Wed Aug 25 01:14:06 2010 +0800

    sections

diff --git a/share/html/Admin/Global/Theme.html b/share/html/Admin/Global/Theme.html
index ad4a4c6..42cf870 100644
--- a/share/html/Admin/Global/Theme.html
+++ b/share/html/Admin/Global/Theme.html
@@ -63,6 +63,12 @@ Upload Logo:
 <input type="submit">
 </form>
 
+<div class="pick-section">
+<select id="section">
+</select>
+</div>
+
+<div class="pick-color">
 % if ($colors) {
 Primary colors:
 %   for (@$colors) {
@@ -75,6 +81,8 @@ Primary colors:
 <span class="color-template" style="background-color: white; color: gray; width: 5em">test</span>
 <span class="color-template" style="background-color: red; color: white; width: 5em">test</span>
 </div>
+</div>
+
 <div class="clear">
 
 Csutom CSS (Advanced):<br />
@@ -90,16 +98,19 @@ Csutom CSS (Advanced):<br />
 
 <script type="text/javascript">
 
-var section = 'main';
-
 var section_css_mapping = {
-    main: ['body',
-           'div#body',
-           'div#quickbar',
-           'input[type="reset"], input[type="submit"], input[class="button"]']
+    default: ['body',
+              'input[type="reset"], input[type="submit"], input[class="button"]'],
+    quickbar: ['div#quickbar'],
+    mainbody: ['div#body']
 };
 
 jQuery(function($) {
+
+    for (var section in section_css_mapping) {
+        $('select#section').append($("<option/>").attr('value', section).text(section));
+    }
+
     $("style#test").text($('#user_css').val());
     $('#try').click(function() {
         $("style#test").text($('#user_css').val());
@@ -109,6 +120,8 @@ jQuery(function($) {
     $('span.color-template').click(function() {
       var bg = $(this).css('background-color');
       var fg = $(this).css('color');
+      var section = $('select#section').val();
+
       var applying = section_css_mapping[section];
       var css = $('#user_css').val();
       if (applying) {

commit 89880ec13d14bd44d6b5528ced3f529accbd2a8b
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Wed Aug 25 01:21:52 2010 +0800

    some usage description

diff --git a/share/html/Admin/Global/Theme.html b/share/html/Admin/Global/Theme.html
index 42cf870..0263dc5 100644
--- a/share/html/Admin/Global/Theme.html
+++ b/share/html/Admin/Global/Theme.html
@@ -64,11 +64,14 @@ Upload Logo:
 </form>
 
 <div class="pick-section">
+<span class="description">Pick a section first</span>
 <select id="section">
 </select>
 </div>
 
 <div class="pick-color">
+<span class="description">Then click on the following color to apply</span>
+<div class="clear">
 % if ($colors) {
 Primary colors:
 %   for (@$colors) {
@@ -77,6 +80,7 @@ Primary colors:
 %   }
 % }
 <div class="clear">
+Pre-defined colors:
 <span class="color-template" style="background-color: white; color: black; width: 5em">test</span>
 <span class="color-template" style="background-color: white; color: gray; width: 5em">test</span>
 <span class="color-template" style="background-color: red; color: white; width: 5em">test</span>

commit 86f07f92b765c56d67fda8463e5cd1fbbba5a363
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Wed Aug 25 01:21:56 2010 +0800

    don't escape css.

diff --git a/share/html/Elements/Header b/share/html/Elements/Header
index ed1b95f..02a8023 100755
--- a/share/html/Elements/Header
+++ b/share/html/Elements/Header
@@ -77,7 +77,7 @@
 % }
 % if (my $attr = $RT::System->FirstAttribute('UserCSS')) {
 <style type="text/css" media="screen">
-<% $attr->Content %>
+<% $attr->Content |n%>
 </style>
 % }
 

commit 8431e768526b040650278121a6849480836403a3
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Wed Aug 25 01:38:30 2010 +0800

    check userlogo attr first.

diff --git a/share/html/Admin/Global/Theme.html b/share/html/Admin/Global/Theme.html
index 0263dc5..ed3165a 100644
--- a/share/html/Admin/Global/Theme.html
+++ b/share/html/Admin/Global/Theme.html
@@ -159,13 +159,14 @@ if (my $file_hash = _UploadedFile( 'logo-upload' )) {
     $img->read(data => $file_hash->{LargeContent} );
 }
 else {
-    my $attr = $RT::System->FirstAttribute('UserLogo');
-    my $content = $attr->Content;
-    if (ref($content) eq 'HASH') {
-        $img->read(data => $content->{data}) or die "Cannot read: ", $img->errstr;
-    }
-    else {
-        $RT::System->DeleteAttribute('UserLogo');
+    if (my $attr = $RT::System->FirstAttribute('UserLogo')) {
+        my $content = $attr->Content;
+        if (ref($content) eq 'HASH') {
+            $img->read(data => $content->{data}) or die "Cannot read: ", $img->errstr;
+        }
+        else {
+            $RT::System->DeleteAttribute('UserLogo');
+        }
     }
 }
 

commit fdf7f302b1360176ee1eb252630a338176197bfa
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Wed Aug 25 01:56:19 2010 +0800

    use Convert::Color instead

diff --git a/share/html/Admin/Global/Theme.html b/share/html/Admin/Global/Theme.html
index ed3165a..42b7cd5 100644
--- a/share/html/Admin/Global/Theme.html
+++ b/share/html/Admin/Global/Theme.html
@@ -147,7 +147,7 @@ jQuery(function($) {
 my @results;
 
 use Imager;
-use Graphics::Color::RGB;
+use Convert::Color;
 
 my $img = Imager->new;
 if (my $file_hash = _UploadedFile( 'logo-upload' )) {
@@ -207,12 +207,11 @@ sub analyze_img {
     for my $i (0..$img->getwidth-1) {
         for my $j (0..$img->getheight-1) {
             my @color = $img->getpixel(x=>$i, y=>$j)->rgba;
-            my $hsl = Graphics::Color::RGB->new( red => $color[0] / 255, green => $color[2] / 255, blue => $color[2] / 255, alpha => $color[3] / 255 )->to_hsl;
-            my ($h,$s,$l) = $hsl->as_array;
             pop @color;
+            my $hsl = Convert::Color->new('rgb:'.join(',',map { $_ / 255 } @color))->convert_to('hsl');
             my $c = join(',', at color);
-            next if $l < 0.1;
-            $color->{$c} ||= { h => $h, s => $s, l => $l, cnt => 0, c => $c};
+            next if $hsl->lightness < 0.1;
+            $color->{$c} ||= { h => $hsl->hue, s => $hsl->saturation, l => $hsl->lightness, cnt => 0, c => $c};
             $color->{$c}->{cnt}++;
         }
     }

commit 43d81fea685c8624622a0bb495f9a0e27f590519
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Wed Aug 25 02:00:05 2010 +0800

    pesky browsers

diff --git a/share/html/Admin/Global/Theme.html b/share/html/Admin/Global/Theme.html
index 42b7cd5..86a0ff3 100644
--- a/share/html/Admin/Global/Theme.html
+++ b/share/html/Admin/Global/Theme.html
@@ -103,10 +103,10 @@ Csutom CSS (Advanced):<br />
 <script type="text/javascript">
 
 var section_css_mapping = {
-    default: ['body',
+    'default': ['body',
               'input[type="reset"], input[type="submit"], input[class="button"]'],
-    quickbar: ['div#quickbar'],
-    mainbody: ['div#body']
+    'quickbar': ['div#quickbar'],
+    'mainbody': ['div#body']
 };
 
 jQuery(function($) {

commit 297201687128ab82e84bf33f5b3dee3dadc722c8
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Wed Aug 25 02:04:24 2010 +0800

    more sections

diff --git a/share/html/Admin/Global/Theme.html b/share/html/Admin/Global/Theme.html
index 86a0ff3..cf47411 100644
--- a/share/html/Admin/Global/Theme.html
+++ b/share/html/Admin/Global/Theme.html
@@ -106,7 +106,9 @@ var section_css_mapping = {
     'default': ['body',
               'input[type="reset"], input[type="submit"], input[class="button"]'],
     'quickbar': ['div#quickbar'],
-    'mainbody': ['div#body']
+    'mainbody': ['div#body'],
+    'navigation': ['div#nav'],
+    'page-menu': ['div#page-navigation ul#page-menu'],
 };
 
 jQuery(function($) {
@@ -192,6 +194,10 @@ div#header h1 { }
 
 div#quickbar {}
 
+div#nav {}
+
+div#page-navigation ul#page-menu {}
+
 input[type="reset"], input[type="submit"], input[class="button"] {}
 ';
 

commit e7dd37fd1cbfd866f88e631570e687fbd1f25e81
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Fri Sep 3 23:40:09 2010 +0800

    actually use user uploaded logo.

diff --git a/share/html/Elements/Logo b/share/html/Elements/Logo
index bf142fd..24fc8b8 100644
--- a/share/html/Elements/Logo
+++ b/share/html/Elements/Logo
@@ -46,11 +46,17 @@
 %# 
 %# END BPS TAGGED BLOCK }}}
 <div id="logo">
+% if ($use_user_logo) {
+<a href="<%$ARGS{'LogoLinkURL'}||RT->Config->Get('LogoLinkURL')%>"><img
+    src="/Helpers/UserLogo"
+    alt="<%loc($ARGS{'LogoAltText'}||RT->Config->Get('LogoAltText'))%>" /></a>
+% } else {
 <a href="<%$ARGS{'LogoLinkURL'}||RT->Config->Get('LogoLinkURL')%>"><img
     src="<%$ARGS{'LogoURL'}||RT->Config->Get('LogoURL')%>"
     alt="<%loc($ARGS{'LogoAltText'}||RT->Config->Get('LogoAltText'))%>"
     width="<%$ARGS{'LogoImageWidth'}||RT->Config->Get('LogoImageWidth')%>"
     height="<%$ARGS{'LogoImageHeight'}||RT->Config->Get('LogoImageHeight')%>" /></a>
+% }
 % if ( $ShowName ) {
     <span class="rtname"><% $Name || loc("RT for [_1]", RT->Config->Get('rtname')) %></span>
 % }
@@ -60,6 +66,9 @@ if ( exists $ARGS{'show_name'} ) {
     $RT::Logger->warning('show_name argument was renamed, use ShowName');
     $ShowName = delete $ARGS{'show_name'};
 }
+
+my $use_user_logo = $RT::System->FirstAttribute('UserLogo');
+
 </%INIT>
 <%ARGS>
 $ShowName => 1
diff --git a/share/html/Helpers/UserLogo b/share/html/Helpers/UserLogo
index 6cf1735..f15a165 100644
--- a/share/html/Helpers/UserLogo
+++ b/share/html/Helpers/UserLogo
@@ -6,6 +6,6 @@ if ( my $attr = $RT::System->FirstAttribute('UserLogo') ) {
     $m->out( $content->{data} );
     $m->abort;
 }
-
+# XXX: 404
 
 </%INIT>

commit c340a8629b3775fb91c7502ec15523a8eb576a91
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Fri Sep 3 23:47:09 2010 +0800

    entry for theme from global admin menu.

diff --git a/share/html/Admin/Global/index.html b/share/html/Admin/Global/index.html
index 1b1f9b0..eca1e61 100755
--- a/share/html/Admin/Global/index.html
+++ b/share/html/Admin/Global/index.html
@@ -79,7 +79,9 @@
                                 text => loc('Modify the default "RT at a glance" view'),
                                 path => '/Admin/Global/MyRT.html',
                       },
-
+                J => { title => loc('Theme'),
+                                path => '/Admin/Global/Theme.html',
+                      },
 
 
 };

commit ffad400f9efb30f91085c6cbd3a3cef810a523f2
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Fri Sep 3 23:50:35 2010 +0800

    make logo analyzer optional.

diff --git a/sbin/rt-test-dependencies.in b/sbin/rt-test-dependencies.in
index 8ee51bd..2deda57 100755
--- a/sbin/rt-test-dependencies.in
+++ b/sbin/rt-test-dependencies.in
@@ -71,6 +71,7 @@ GetOptions(
     'with-GRAPHVIZ',
     'with-GD',
     'with-DASHBOARDS',
+    'with-USERLOGO',
 
     'download=s',
     'repository=s',
@@ -97,7 +98,8 @@ my %default = (
     'with-SMTP' => 1,
     'with-GRAPHVIZ' => @RT_GRAPHVIZ@,
     'with-GD' => @RT_GD@,
-    'with-DASHBOARDS' => 1
+    'with-DASHBOARDS' => 1,
+    'with-USERLOGO' => 1,
 );
 $args{$_} = $default{$_} foreach grep !exists $args{$_}, keys %default;
 
@@ -349,6 +351,11 @@ GD::Graph
 GD::Text
 .
 
+$deps{'USERLOGO'} = [ text_to_hash( << '.') ];
+Imager
+Convert::Color
+.
+
 my %AVOID = (
     'DBD::Oracle' => [qw(1.23)],
 );
diff --git a/share/html/Admin/Global/Theme.html b/share/html/Admin/Global/Theme.html
index cf47411..5ffbb24 100644
--- a/share/html/Admin/Global/Theme.html
+++ b/share/html/Admin/Global/Theme.html
@@ -148,8 +148,8 @@ jQuery(function($) {
 <%INIT>
 my @results;
 
-use Imager;
-use Convert::Color;
+my $has_color_analyzer =
+eval { require Imger; require Convert::Color; 1 };
 
 my $img = Imager->new;
 if (my $file_hash = _UploadedFile( 'logo-upload' )) {
@@ -203,7 +203,9 @@ input[type="reset"], input[type="submit"], input[class="button"] {}
 
 }
 
-my $colors = $img ? analyze_img($img) : undef;
+# XXX: move this to some other modules
+
+my $colors = $img && $has_color_analyzer? analyze_img($img) : undef;
 use List::MoreUtils qw(uniq);
 
 sub analyze_img {

commit 8747ab26c6eb349bd210ba7ac68e7d653bb864a9
Merge: d885f6b df8d7d0
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Mon Sep 6 14:13:56 2010 -0400

    Merge branch 'integrate-mobile-ui' into 3.9-trunk
    
    * integrate-mobile-ui:
      Initial import of the mobile UI extension as a core feature


commit a7f761ca40657ecec634ca32c9766be8fab3252e
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Mon Sep 6 14:14:23 2010 -0400

    Added S60 as a mobile useragent

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index a5a5df8..a946176 100755
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -662,7 +662,7 @@ sub MobileClient {
     my $self = shift;
 
 
-if (($ENV{'HTTP_USER_AGENT'} || '') =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS)/io && !$HTML::Mason::Commands::session{'NotMobile'})  {
+if (($ENV{'HTTP_USER_AGENT'} || '') =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS|S60)/io && !$HTML::Mason::Commands::session{'NotMobile'})  {
     return 1;
 } else {
     return undef;

commit 20add7d8d5fc55f979766120a7770c5f703564fe
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Tue Sep 7 21:50:18 2010 +0800

    color picker and a bit more layout cleaning

diff --git a/share/html/Admin/Global/Theme.html b/share/html/Admin/Global/Theme.html
index 5ffbb24..48f0a03 100644
--- a/share/html/Admin/Global/Theme.html
+++ b/share/html/Admin/Global/Theme.html
@@ -51,9 +51,14 @@
     Title => loc("Theme") &>
 <& /Elements/ListActions, actions => \@results &>
 
+
+<script type="text/javascript" src="/NoAuth/js/farbtastic.js"></script>
+<link rel="stylesheet" href="/NoAuth/css/farbtastic.css" type="text/css" />
 <style type="text/css" media="screen" id="test">
 </style>
 
+<h1>Logo</h3>
+<div class="upload-logo">
 Upload Logo:
 % if ($img) {
 <img src="/Helpers/UserLogo?<% time() %>" />
@@ -62,44 +67,48 @@ Upload Logo:
 <input type="file" name="logo-upload" />
 <input type="submit">
 </form>
+</div>
 
-<div class="pick-section">
-<span class="description">Pick a section first</span>
+<h1>Customize Theme</h3>
+<div class="pick-section" style="float: left; width: 40%">
+<span class="description">Select a section</span>
 <select id="section">
 </select>
 </div>
 
-<div class="pick-color">
-<span class="description">Then click on the following color to apply</span>
-<div class="clear">
+<div class="pick-color" style="float: left; width: 50%">
+<span class="description">click on the color to apply to the selected section</span>
+<div class="clear"></div>
 % if ($colors) {
-Primary colors:
+Primary colors:<br/>
 %   for (@$colors) {
 %     my $fg = $_->{l} >= 0.6 ? 'black' : 'white';
-<span class="color-template" style="background-color: rgb(<% $_->{c} %>); color: <% $fg %>; width: 5em">test l=<% $_->{l}%>></span>
+<span class="color-template" style="background-color: rgb(<% $_->{c} %>); color: <% $fg %>; width: 5em">test l=<% int(100*$_->{l})/100%></span>
 %   }
 % }
 <div class="clear">
-Pre-defined colors:
-<span class="color-template" style="background-color: white; color: black; width: 5em">test</span>
-<span class="color-template" style="background-color: white; color: gray; width: 5em">test</span>
-<span class="color-template" style="background-color: red; color: white; width: 5em">test</span>
+<div id="picker"></div>
 </div>
+
+
 </div>
 
-<div class="clear">
+<div class="clear"></div>
+
+<h1>Custom CSS (Advanced)</h1>
+<div class="custom-css">
 
-Csutom CSS (Advanced):<br />
 
 <form method="POST">
 
 <textarea rows=30 cols=60 id="user_css" name="user_css"><% $user_css %></textarea>
 
 <input id="try" type="button" value="Try">
-<input name="reset_css" value="Reset CSS" type="submit">
+<input id="reset" type="reset" value="Reset" type="submit">
+<input name="reset_css" value="Reset to default RT Theme" type="submit">
 <input value="Save" type="submit">
 </form>
-
+</div>
 <script type="text/javascript">
 
 var section_css_mapping = {
@@ -122,10 +131,14 @@ jQuery(function($) {
         $("style#test").text($('#user_css').val());
     });
 
+    $('#reset').click(function() {
+        setTimeout(function() {
+          $("style#test").text($('#user_css').val());
+        }, 1000);
+    });
+
 
-    $('span.color-template').click(function() {
-      var bg = $(this).css('background-color');
-      var fg = $(this).css('color');
+    function change_color(bg, fg) {
       var section = $('select#section').val();
 
       var applying = section_css_mapping[section];
@@ -138,7 +151,12 @@ jQuery(function($) {
       }
       $('#user_css').val(css);
       $("style#test").text(css);
+    }
+
+    $('#picker').farbtastic(function(color){ change_color(color, this.hsl[2] > 0.5 ? '#000' : '#fff') });
 
+    $('span.color-template').click(function() {
+      change_color($(this).css('background-color'), $(this).css('color'));
   });
 
 
@@ -149,7 +167,7 @@ jQuery(function($) {
 my @results;
 
 my $has_color_analyzer =
-eval { require Imger; require Convert::Color; 1 };
+eval { require Imager; require Convert::Color; 1 };
 
 my $img = Imager->new;
 if (my $file_hash = _UploadedFile( 'logo-upload' )) {
diff --git a/share/html/NoAuth/css/farbtastic.css b/share/html/NoAuth/css/farbtastic.css
new file mode 100644
index 0000000..17309b9
--- /dev/null
+++ b/share/html/NoAuth/css/farbtastic.css
@@ -0,0 +1,51 @@
+/**
+ * Farbtastic Color Picker 1.2
+ * © 2008 Steven Wittens
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+.farbtastic {
+  position: relative;
+}
+.farbtastic * {
+  position: absolute;
+  cursor: crosshair;
+}
+.farbtastic, .farbtastic .wheel {
+  width: 195px;
+  height: 195px;
+}
+.farbtastic .color, .farbtastic .overlay {
+  top: 47px;
+  left: 47px;
+  width: 101px;
+  height: 101px;
+}
+.farbtastic .wheel {
+  background: url(/NoAuth/images/farbtastic/wheel.png) no-repeat;
+  width: 195px;
+  height: 195px;
+}
+.farbtastic .overlay {
+  background: url(/NoAuth/images/farbtastic/mask.png) no-repeat;
+}
+.farbtastic .marker {
+  width: 17px;
+  height: 17px;
+  margin: -8px 0 0 -8px;
+  overflow: hidden; 
+  background: url(/NoAuth/images/farbtastic/marker.png) no-repeat;
+}
+
diff --git a/share/html/NoAuth/images/farbtastic/marker.png b/share/html/NoAuth/images/farbtastic/marker.png
new file mode 100755
index 0000000..3929bbb
Binary files /dev/null and b/share/html/NoAuth/images/farbtastic/marker.png differ
diff --git a/share/html/NoAuth/images/farbtastic/mask.png b/share/html/NoAuth/images/farbtastic/mask.png
new file mode 100644
index 0000000..b0a4d40
Binary files /dev/null and b/share/html/NoAuth/images/farbtastic/mask.png differ
diff --git a/share/html/NoAuth/images/farbtastic/wheel.png b/share/html/NoAuth/images/farbtastic/wheel.png
new file mode 100644
index 0000000..97b343d
Binary files /dev/null and b/share/html/NoAuth/images/farbtastic/wheel.png differ
diff --git a/share/html/NoAuth/js/farbtastic.js b/share/html/NoAuth/js/farbtastic.js
new file mode 100644
index 0000000..7499b9c
--- /dev/null
+++ b/share/html/NoAuth/js/farbtastic.js
@@ -0,0 +1,347 @@
+/**
+ * Farbtastic Color Picker 1.2
+ * © 2008 Steven Wittens
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+(function ($){
+jQuery.fn.farbtastic = function (callback) {
+  $.farbtastic(this, callback);
+  return this;
+};
+
+jQuery.farbtastic = function (container, callback) {
+  var container = $(container).get(0);
+  return container.farbtastic || (container.farbtastic = new jQuery._farbtastic(container, callback));
+}
+
+jQuery._farbtastic = function (container, callback) {
+  // Store farbtastic object
+  var fb = this;
+
+  // Insert markup
+  $(container).html('<div class="farbtastic"><div class="color"></div><div class="wheel"></div><div class="overlay"></div><div class="h-marker marker"></div><div class="sl-marker marker"></div></div>');
+  var e = $('.farbtastic', container);
+  fb.wheel = $('.wheel', container).get(0);
+  // Dimensions
+  fb.radius = 84;
+  fb.square = 100;
+  fb.width = 194;
+
+  // Fix background PNGs in IE6
+  if (navigator.appVersion.match(/MSIE [0-6]\./)) {
+    $('*', e).each(function () {
+      if (this.currentStyle.backgroundImage != 'none') {
+        var image = this.currentStyle.backgroundImage;
+        image = this.currentStyle.backgroundImage.substring(5, image.length - 2);
+        $(this).css({
+          'backgroundImage': 'none',
+          'filter': "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image + "')"
+        });
+      }
+    });
+  }
+
+  /**
+   * Link to the given element(s) or callback.
+   */
+  fb.linkTo = function (callback) {
+    // Unbind previous nodes
+    if (typeof fb.callback == 'object') {
+      $(fb.callback).unbind('keyup', fb.updateValue);
+    }
+
+    // Reset color
+    fb.color = null;
+
+    // Bind callback or elements
+    if (typeof callback == 'function') {
+      fb.callback = callback;
+    }
+    else if (typeof callback == 'object' || typeof callback == 'string') {
+      fb.callback = $(callback);
+      fb.callback.bind('keyup', fb.updateValue);
+      if (fb.callback.get(0).value) {
+        fb.setColor(fb.callback.get(0).value);
+      }
+    }
+    return this;
+  }
+  fb.updateValue = function (event) {
+    if (this.value && this.value != fb.color) {
+      fb.setColor(this.value);
+    }
+  }
+
+  /**
+   * Change color with HTML syntax #123456
+   */
+  fb.setColor = function (color) {
+    var unpack = fb.unpack(color);
+    if (fb.color != color && unpack) {
+      fb.color = color;
+      fb.rgb = unpack;
+      fb.hsl = fb.RGBToHSL(fb.rgb);
+      fb.updateDisplay();
+    }
+    return this;
+  }
+
+  /**
+   * Change color with HSL triplet [0..1, 0..1, 0..1]
+   */
+  fb.setHSL = function (hsl) {
+    fb.hsl = hsl;
+    fb.rgb = fb.HSLToRGB(hsl);
+    fb.color = fb.pack(fb.rgb);
+    fb.updateDisplay();
+    return this;
+  }
+
+  /////////////////////////////////////////////////////
+
+  /**
+   * Retrieve the coordinates of the given event relative to the center
+   * of the widget.
+   */
+  fb.widgetCoords = function (event) {
+    var x, y;
+    var el = event.target || event.srcElement;
+    var reference = fb.wheel;
+
+    if (typeof event.offsetX != 'undefined') {
+      // Use offset coordinates and find common offsetParent
+      var pos = { x: event.offsetX, y: event.offsetY };
+
+      // Send the coordinates upwards through the offsetParent chain.
+      var e = el;
+      while (e) {
+        e.mouseX = pos.x;
+        e.mouseY = pos.y;
+        pos.x += e.offsetLeft;
+        pos.y += e.offsetTop;
+        e = e.offsetParent;
+      }
+
+      // Look for the coordinates starting from the wheel widget.
+      var e = reference;
+      var offset = { x: 0, y: 0 }
+      while (e) {
+        if (typeof e.mouseX != 'undefined') {
+          x = e.mouseX - offset.x;
+          y = e.mouseY - offset.y;
+          break;
+        }
+        offset.x += e.offsetLeft;
+        offset.y += e.offsetTop;
+        e = e.offsetParent;
+      }
+
+      // Reset stored coordinates
+      e = el;
+      while (e) {
+        e.mouseX = undefined;
+        e.mouseY = undefined;
+        e = e.offsetParent;
+      }
+    }
+    else {
+      // Use absolute coordinates
+      var pos = fb.absolutePosition(reference);
+      x = (event.pageX || 0*(event.clientX + $('html').get(0).scrollLeft)) - pos.x;
+      y = (event.pageY || 0*(event.clientY + $('html').get(0).scrollTop)) - pos.y;
+    }
+    // Subtract distance to middle
+    return { x: x - fb.width / 2, y: y - fb.width / 2 };
+  }
+
+  /**
+   * Mousedown handler
+   */
+  fb.mousedown = function (event) {
+    // Capture mouse
+    if (!document.dragging) {
+      $(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup);
+      document.dragging = true;
+    }
+
+    // Check which area is being dragged
+    var pos = fb.widgetCoords(event);
+    fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) * 2 > fb.square;
+
+    // Process
+    fb.mousemove(event);
+    return false;
+  }
+
+  /**
+   * Mousemove handler
+   */
+  fb.mousemove = function (event) {
+    // Get coordinates relative to color picker center
+    var pos = fb.widgetCoords(event);
+
+    // Set new HSL parameters
+    if (fb.circleDrag) {
+      var hue = Math.atan2(pos.x, -pos.y) / 6.28;
+      if (hue < 0) hue += 1;
+      fb.setHSL([hue, fb.hsl[1], fb.hsl[2]]);
+    }
+    else {
+      var sat = Math.max(0, Math.min(1, -(pos.x / fb.square) + .5));
+      var lum = Math.max(0, Math.min(1, -(pos.y / fb.square) + .5));
+      fb.setHSL([fb.hsl[0], sat, lum]);
+    }
+    return false;
+  }
+
+  /**
+   * Mouseup handler
+   */
+  fb.mouseup = function () {
+    // Uncapture mouse
+    $(document).unbind('mousemove', fb.mousemove);
+    $(document).unbind('mouseup', fb.mouseup);
+    document.dragging = false;
+  }
+
+  /**
+   * Update the markers and styles
+   */
+  fb.updateDisplay = function () {
+    // Markers
+    var angle = fb.hsl[0] * 6.28;
+    $('.h-marker', e).css({
+      left: Math.round(Math.sin(angle) * fb.radius + fb.width / 2) + 'px',
+      top: Math.round(-Math.cos(angle) * fb.radius + fb.width / 2) + 'px'
+    });
+
+    $('.sl-marker', e).css({
+      left: Math.round(fb.square * (.5 - fb.hsl[1]) + fb.width / 2) + 'px',
+      top: Math.round(fb.square * (.5 - fb.hsl[2]) + fb.width / 2) + 'px'
+    });
+
+    // Saturation/Luminance gradient
+    $('.color', e).css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5])));
+
+    // Linked elements or callback
+    if (typeof fb.callback == 'object') {
+      // Set background/foreground color
+      $(fb.callback).css({
+        backgroundColor: fb.color,
+        color: fb.hsl[2] > 0.5 ? '#000' : '#fff'
+      });
+
+      // Change linked value
+      $(fb.callback).each(function() {
+        if (this.value && this.value != fb.color) {
+          this.value = fb.color;
+        }
+      });
+    }
+    else if (typeof fb.callback == 'function') {
+      fb.callback.call(fb, fb.color);
+    }
+  }
+
+  /**
+   * Get absolute position of element
+   */
+  fb.absolutePosition = function (el) {
+    var r = { x: el.offsetLeft, y: el.offsetTop };
+    // Resolve relative to offsetParent
+    if (el.offsetParent) {
+      var tmp = fb.absolutePosition(el.offsetParent);
+      r.x += tmp.x;
+      r.y += tmp.y;
+    }
+    return r;
+  };
+
+  /* Various color utility functions */
+  fb.pack = function (rgb) {
+    var r = Math.round(rgb[0] * 255);
+    var g = Math.round(rgb[1] * 255);
+    var b = Math.round(rgb[2] * 255);
+    return '#' + (r < 16 ? '0' : '') + r.toString(16) +
+           (g < 16 ? '0' : '') + g.toString(16) +
+           (b < 16 ? '0' : '') + b.toString(16);
+  }
+
+  fb.unpack = function (color) {
+    if (color.length == 7) {
+      return [parseInt('0x' + color.substring(1, 3)) / 255,
+        parseInt('0x' + color.substring(3, 5)) / 255,
+        parseInt('0x' + color.substring(5, 7)) / 255];
+    }
+    else if (color.length == 4) {
+      return [parseInt('0x' + color.substring(1, 2)) / 15,
+        parseInt('0x' + color.substring(2, 3)) / 15,
+        parseInt('0x' + color.substring(3, 4)) / 15];
+    }
+  }
+
+  fb.HSLToRGB = function (hsl) {
+    var m1, m2, r, g, b;
+    var h = hsl[0], s = hsl[1], l = hsl[2];
+    m2 = (l <= 0.5) ? l * (s + 1) : l + s - l*s;
+    m1 = l * 2 - m2;
+    return [this.hueToRGB(m1, m2, h+0.33333),
+        this.hueToRGB(m1, m2, h),
+        this.hueToRGB(m1, m2, h-0.33333)];
+  }
+
+  fb.hueToRGB = function (m1, m2, h) {
+    h = (h < 0) ? h + 1 : ((h > 1) ? h - 1 : h);
+    if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
+    if (h * 2 < 1) return m2;
+    if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6;
+    return m1;
+  }
+
+  fb.RGBToHSL = function (rgb) {
+    var min, max, delta, h, s, l;
+    var r = rgb[0], g = rgb[1], b = rgb[2];
+    min = Math.min(r, Math.min(g, b));
+    max = Math.max(r, Math.max(g, b));
+    delta = max - min;
+    l = (min + max) / 2;
+    s = 0;
+    if (l > 0 && l < 1) {
+      s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l));
+    }
+    h = 0;
+    if (delta > 0) {
+      if (max == r && max != g) h += (g - b) / delta;
+      if (max == g && max != b) h += (2 + (b - r) / delta);
+      if (max == b && max != r) h += (4 + (r - g) / delta);
+      h /= 6;
+    }
+    return [h, s, l];
+  }
+
+  // Install mousedown handler (the others are set on the document on-demand)
+  $('*', e).mousedown(fb.mousedown);
+
+    // Init color
+  fb.setColor('#000000');
+
+  // Set linked elements/callback
+  if (callback) {
+    fb.linkTo(callback);
+  }
+}
+})(jQuery);
\ No newline at end of file

commit 1d1ba7bd23e6587d7dd29978b36accd1fcacb8f4
Merge: a7f761c 20add7d
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Tue Sep 7 21:50:50 2010 +0800

    Merge branch 'custom-css' into 3.9-trunk


commit 229f48969a89ebc89a2dd8ee578439a1937a7bb9
Author: Emmanuel Lacour <elacour at easter-eggs.com>
Date:   Tue Sep 7 17:43:48 2010 +0200

    New theme editor require List::MoreUtils

diff --git a/sbin/rt-test-dependencies.in b/sbin/rt-test-dependencies.in
index 44513b1..ddbe5b4 100755
--- a/sbin/rt-test-dependencies.in
+++ b/sbin/rt-test-dependencies.in
@@ -208,6 +208,7 @@ File::Glob
 Devel::StackTrace 1.19
 Text::Password::Pronounceable
 Devel::GlobalDestruction
+List::MoreUtils
 .
 
 $deps{'MASON'} = [ text_to_hash( << '.') ];

commit 5740b7ad08bbf4b45154555e8f6d49d917670159
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Fri Sep 3 16:59:24 2010 -0400

    Revoking an invalid right shouldn't be an error condition
    
    Instead, warn appropriately and then return success.  HasRight and
    friends check for right validity as well, so there's no harm in ignoring
    invalid rights in RevokeRight.

diff --git a/lib/RT/Principal_Overlay.pm b/lib/RT/Principal_Overlay.pm
index e57f6a3..65992b5 100755
--- a/lib/RT/Principal_Overlay.pm
+++ b/lib/RT/Principal_Overlay.pm
@@ -210,6 +210,12 @@ sub RevokeRight {
         PrincipalType => $type,
         PrincipalId   => $self->Id
     );
+
+    if ( not $status and $msg =~ /Invalid right/ ) {
+        $RT::Logger->warn("Tried to revoke the invalid right '$args{Right}', ignoring it.");
+        return (1);
+    }
+
     return ($status, $msg) unless $status;
     return $ace->Delete;
 }

commit d767fd5d0924666abb5ab864005210553d3717c9
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Tue Sep 7 10:54:23 2010 -0400

    Return the group members alias from within WhoHaveGroupRight
    
    This lets us extend the query outside of the methods provided without
    duplicating a join.

diff --git a/lib/RT/Users_Overlay.pm b/lib/RT/Users_Overlay.pm
index decfe4b..1896d0c 100755
--- a/lib/RT/Users_Overlay.pm
+++ b/lib/RT/Users_Overlay.pm
@@ -461,6 +461,7 @@ sub _JoinGroupMembersForGroupRights
                   VALUE => "$group_members.GroupId",
                   QUOTEVALUE => 0,
                 );
+    return $group_members;
 }
 
 # XXX: should be generalized
@@ -504,7 +505,7 @@ sub WhoHaveGroupRight
     }
     $self->_AddSubClause( "WhichObject", "($check_objects)" );
     
-    $self->_JoinGroupMembersForGroupRights( %args, ACLAlias => $acl );
+    my $group_members = $self->_JoinGroupMembersForGroupRights( %args, ACLAlias => $acl );
     # Find only members of groups that have the right.
     $self->Limit( ALIAS => $acl,
                   FIELD => 'PrincipalType',
@@ -517,7 +518,7 @@ sub WhoHaveGroupRight
                   OPERATOR => '!=',
                   VALUE => $RT::SystemUser->id
                 );
-    return;
+    return $group_members;
 }
 
 # {{{ WhoBelongToGroups

commit 1e13c5e996e3123d0e956fd8877eee56293c67ee
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Tue Sep 7 10:57:01 2010 -0400

    Actually restrict the user list in the rights editor to those with rights
    
    We limit on groups of domain ACLEquivalence and type UserEquiv in order
    to do this.

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index a946176..cdc8634 100755
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -2230,12 +2230,25 @@ sub GetPrincipalsMap {
             $Users->OrderBy( FIELD => 'Name', ORDER => 'ASC' );
 
             # Only show users who have rights granted on this object
-            $Users->WhoHaveGroupRight(
+            my $group_members = $Users->WhoHaveGroupRight(
                 Right   => '',
                 Object  => $object,
-                IncludeSystemRights => 0
+                IncludeSystemRights => 0,
+                IncludeSubgroupMembers => 0,
             );
 
+            # Limit to UserEquiv groups
+            my $groups = $Users->NewAlias('Groups');
+            $Users->Join(
+                ALIAS1 => $groups,
+                FIELD1 => 'id',
+                ALIAS2 => $group_members,
+                FIELD2 => 'GroupId'
+            );
+            $Users->Limit( ALIAS => $groups, FIELD => 'Domain', VALUE => 'ACLEquivalence' );
+            $Users->Limit( ALIAS => $groups, FIELD => 'Type', VALUE => 'UserEquiv' );
+
+
             my $display = sub {
                 $m->scomp('/Elements/ShowUser', User => $_[0], NoEscape => 1)
             };

commit 96acb1b150afc409a9cc94df664faf7ed795ff09
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Tue Sep 7 11:50:37 2010 -0400

    Kill the orange from the rights editor UI

diff --git a/share/html/NoAuth/css/base/rights-editor.css b/share/html/NoAuth/css/base/rights-editor.css
index 937123c..b8fc3b6 100644
--- a/share/html/NoAuth/css/base/rights-editor.css
+++ b/share/html/NoAuth/css/base/rights-editor.css
@@ -70,4 +70,33 @@ li.category ~ li.category {
 
 .category-tabs {
     width: 100%;
+    border: none;
+    background: none;
+}
+
+.category-tabs .ui-tabs-nav {
+    border: none;
+    background: none;
 }
+
+.category-tabs .ui-tabs-panel {
+    background: none;
+    border: 1px solid #aaa;
+}
+
+.rights-editor li.ui-tabs-selected {
+    background: white !important;
+    color: #222 !important;
+    border-color: #aaa !important; 
+}
+
+.rights-editor .ui-state-active a,
+.rights-editor .ui-state-hover a {
+    color: #222 !important;
+}
+
+.rights-editor .category-tabs li.ui-state-hover {
+    background: #f6f6f6;
+    border-color: #aaa !important;
+}
+

commit 045a925421a6f8fdddc915deadddb8eb6d20a4e9
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Tue Sep 7 13:00:54 2010 -0400

    t/i18n/default.t now requires initialdata.
    
    clkao didn't run tests before he commited.

diff --git a/t/i18n/default.t b/t/i18n/default.t
index cc6f136..747e9f4 100644
--- a/t/i18n/default.t
+++ b/t/i18n/default.t
@@ -2,7 +2,7 @@
 use strict;
 use warnings;
 
-use RT::Test noinitialdata => 1, tests => 8;
+use RT::Test noinitialdata => 0, tests => 8;
 
 my ($baseurl, $m) = RT::Test->started_ok;
 $m->get_ok('/');

commit a2f5c421711acf53426c23cc763d87af6976eb4b
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Tue Sep 7 13:39:05 2010 -0400

    Ward against an undef warning.

diff --git a/share/html/Elements/ListMenu b/share/html/Elements/ListMenu
index 1a0d4eb..1f9dbd1 100644
--- a/share/html/Elements/ListMenu
+++ b/share/html/Elements/ListMenu
@@ -51,7 +51,7 @@ $items
 <ul class="list-menu">
 %    foreach my $key (sort keys %$items) {
 <li><span class="menu-item"><a href="<%RT->Config->Get('WebPath')%><%$items->{$key}->{'path'}|n %>"><%$items->{$key}->{'title'}%></a></span><br />
-<span class="description"><%$items->{$key}->{description} || $items->{$key}->{text} %></span>
+<span class="description"><%$items->{$key}->{description} || $items->{$key}->{text} || '' %></span>
 
 </li>
 %}

-----------------------------------------------------------------------


More information about the Rt-commit mailing list