commit/mtrack: 24 new changesets

1 view
Skip to first unread message

Bitbucket

unread,
Apr 23, 2012, 12:58:28 AM4/23/12
to mtr...@googlegroups.com
24 new commits in mtrack:


https://bitbucket.org/wez/mtrack/changeset/ab0884e37311/
changeset: ab0884e37311
user: wez
date: 2012-04-21 02:54:53
summary: on install, put the wiki directly in var/repos/default/wiki instead of
in var and having the other part of the system fix that up with a
symlink.

Have a clearer notice for folks that re-run the install after
encountering an error.

More thorough extension pre-requisite checking.
affected #: 1 file

diff -r da58689c5ca03d3acdf3418ab3efff0dd37eac06 -r ab0884e373112414f3f3b61e589756c8783919ba bin/init.php
--- a/bin/init.php
+++ b/bin/init.php
@@ -16,9 +16,22 @@
}

/* People doing this are not necessarily sane, make sure we have PDO and
- * pdo_sqlite */
-if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) {
- echo "Mtrack requires PDO and pdo_sqlite to function\n";
+ * pdo_sqlite.
+ * Furthermore, poor folks on FreeBSD have to put up with a PHP that has
+ * the core default components stripped out and built shared. Help
+ * them understand what they need. */
+$required_extensions = array(
+ 'PDO', 'pdo_sqlite', 'ctype', 'Reflection', 'json', 'pcre',
+ 'session', 'dom', 'SimpleXML',
+);
+$pre_req_not_met = false;
+foreach ($required_extensions as $ext) {
+ if (!extension_loaded($ext)) {
+ echo "The '$ext' extension is required\n";
+ $pre_req_not_met = true;
+ }
+}
+if ($pre_req_not_met) {
exit(1);
}

@@ -173,8 +186,8 @@
usage("Unhandled arguments: ".implode(", ", $args));
}

-if (file_exists("$vardir/mtrac.db")) {
- echo "Nothing to do (already configured)\n";
+if (file_exists("$vardir/mtrac.db") || file_exists($config_file_name)) {
+ echo "Already configured. To re-install, remove $vardir/mtrac.db and $config_file_name\n";
exit(1);
}

@@ -463,7 +476,7 @@
$r = new MTrackRepo;
$r->shortname = 'wiki';
$r->scmtype = $wiki_repo_type;
- $r->repopath = realpath($vardir) . DIRECTORY_SEPARATOR . 'wiki';
+ $r->repopath = realpath($vardir) . DIRECTORY_SEPARATOR . 'default/wiki';
$r->description = 'The mtrack wiki pages are stored here';
echo " ** Creating repo 'wiki' of type $r->scmtype to hold Wiki content at $r->repopath\n";
echo " ** (use --repo option to specify an alternate location)\n";



https://bitbucket.org/wez/mtrack/changeset/e25bdf0b0389/
changeset: e25bdf0b0389
user: wez
date: 2012-04-21 04:09:11
summary: tidy up install/auth misconfiguration alerts.
affected #: 2 files

diff -r ab0884e373112414f3f3b61e589756c8783919ba -r e25bdf0b0389730127acb945e286989226c7398e inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -187,15 +187,17 @@

echo mtrack_nav('mainnav', $nav);
}
+ $party_addrs = MTrackConfig::get('core', 'admin_party_remote_address');
if (MTrackConfig::get('core', 'admin_party') == 1 &&
MTrackAuth::whoami() == 'adminparty' &&
- in_array($_SERVER['REMOTE_ADDR'], explode(',', MTrackConfig::get('core', 'admin_party_remote_address')))) {
+ in_array($_SERVER['REMOTE_ADDR'], explode(',', $party_addrs))) {
echo <<<HTML
-<div class='ui-state-error ui-corner-all'>
- <span class='ui-icon ui-icon-alert'></span>
- <b>Welcome to the admin party!</b> Authentication is not yet configured;
- while it is in this state, any user connecting from the localhost
- address is treated as having admin rights (that includes you, and this
+<div class='alert alert-error'>
+ <a class='close' data-dismiss='alert'>×</a>
+ <h4 class='alert-heading'>Welcome to the admin party!</h4>
+ Authentication is not yet configured;
+ while it is in this state, any user connecting from <b>$party_addrs</b>
+ is treated as having admin rights (that includes you, and this
is why you are seeing this message). All other users are treated
as anonymous users.<br><b><a href="{$ABSWEB}admin/auth.php">Click here to Configure Authentication</a></b>
@@ -205,20 +207,22 @@
MTrackConfig::get('core', 'admin_party') == 1)
{
$localaddr = preg_replace('@^(https?://)([^/]+)/(.*)$@',
- "\\1localhost/\\3", $ABSWEB);
+ "\${1}127.0.0.1/\\3", $ABSWEB);
+ $remoteaddr = htmlentities($_SERVER['REMOTE_ADDR']);

echo <<<HTML
-<div class='ui-state-highlight ui-corner-all'>
- <span class='ui-icon ui-icon-info'></span>
- <b>Authentication is not yet configured</b>. If you are the admin,
+<div class='alert alert-error'>
+ <a class='close' data-dismiss='alert'>×</a>
+ <h4 class='alert-heading'>Authentication is not yet configured</h4>
+ If you are the admin,
you should use the <b><a href="$localaddr">localhost address</a></b>
- to reach the system and configure it.
+ to reach the system and configure it, or configure the <b>admin_party_remote_address</b> option to include <b>$remoteaddr</b>.
</div>
HTML;
} elseif (!MTrackAuth::isAuthConfigured()) {
echo <<<HTML
-<div class='ui-state-highlight ui-corner-all'>
- <span class='ui-icon ui-icon-info'></span>
+<div class='alert alert-error'>
+ <a class='close' data-dismiss='alert'>×</a><b>Authentication is not yet configured</b>. If you are the admin,
you will need to edit the config.ini file to configure authentication.
</div>
@@ -227,10 +231,9 @@

if (preg_match("/(on|true|1)/i", ini_get('magic_quotes_gpc'))) {
echo <<<HTML
-<div class='ui-state-error ui-corner-all'>
- <span class='ui-icon ui-icon-alert'></span>
+<div class='alert alert-error'><b>magic_quotes_gpc</b> is enabled. This causes mtrack not to work.
- Please disable this setting in your server configuration.
+ Disable this setting in your server configuration.
</div>
HTML;



diff -r ab0884e373112414f3f3b61e589756c8783919ba -r e25bdf0b0389730127acb945e286989226c7398e web/css/tw-bootstrap.css
--- a/web/css/tw-bootstrap.css
+++ b/web/css/tw-bootstrap.css
@@ -1244,3 +1244,51 @@
background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
}

+.alert {
+ padding: 8px 35px 8px 14px;
+ margin-bottom: 18px;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ background-color: #fcf8e3;
+ border: 1px solid #fbeed5;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ color: #c09853;
+}
+.alert-heading {
+ color: inherit;
+}
+.alert .close {
+ position: relative;
+ top: -2px;
+ right: -21px;
+ line-height: 18px;
+}
+.alert-success {
+ background-color: #dff0d8;
+ border-color: #d6e9c6;
+ color: #468847;
+}
+.alert-danger,
+.alert-error {
+ background-color: #f2dede;
+ border-color: #eed3d7;
+ color: #b94a48;
+}
+.alert-info {
+ background-color: #d9edf7;
+ border-color: #bce8f1;
+ color: #3a87ad;
+}
+.alert-block {
+ padding-top: 14px;
+ padding-bottom: 14px;
+}
+.alert-block > p,
+.alert-block > ul {
+ margin-bottom: 0;
+}
+.alert-block p + p {
+ margin-top: 5px;
+}
+



https://bitbucket.org/wez/mtrack/changeset/bbacba94edb5/
changeset: bbacba94edb5
user: wez
date: 2012-04-21 04:35:38
summary: don't show the admin part alert on the auth page; it gets in the way
affected #: 2 files

diff -r e25bdf0b0389730127acb945e286989226c7398e -r bbacba94edb55867f4722fc2c3bd3012be29bf09 inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -191,18 +191,24 @@
if (MTrackConfig::get('core', 'admin_party') == 1 &&
MTrackAuth::whoami() == 'adminparty' &&
in_array($_SERVER['REMOTE_ADDR'], explode(',', $party_addrs))) {
+
+ if ($_SERVER['SCRIPT_FILENAME'] !=
+ dirname(dirname(__FILE__)) . '/web/admin/auth.php') {
+
echo <<<HTML
<div class='alert alert-error'>
- <a class='close' data-dismiss='alert'>×</a>
+ <a class='close' data-dismiss='alert'>&times;</a><h4 class='alert-heading'>Welcome to the admin party!</h4>
+ <p>
Authentication is not yet configured;
while it is in this state, any user connecting from <b>$party_addrs</b>
is treated as having admin rights (that includes you, and this
is why you are seeing this message). All other users are treated
- as anonymous users.<br>
- <b><a href="{$ABSWEB}admin/auth.php">Click here to Configure Authentication</a></b>
+ as anonymous users.</p>
+ <p><a class='btn btn-danger' href="{$ABSWEB}admin/auth.php">Click here to Configure Authentication</a></p></div>
HTML;
+ }
} elseif (!MTrackAuth::isAuthConfigured() &&
MTrackConfig::get('core', 'admin_party') == 1)
{
@@ -212,7 +218,7 @@

echo <<<HTML
<div class='alert alert-error'>
- <a class='close' data-dismiss='alert'>×</a>
+ <a class='close' data-dismiss='alert'>&times;</a><h4 class='alert-heading'>Authentication is not yet configured</h4>
If you are the admin,
you should use the <b><a href="$localaddr">localhost address</a></b>
@@ -222,7 +228,7 @@
} elseif (!MTrackAuth::isAuthConfigured()) {
echo <<<HTML
<div class='alert alert-error'>
- <a class='close' data-dismiss='alert'>×</a>
+ <a class='close' data-dismiss='alert'>&times;</a><b>Authentication is not yet configured</b>. If you are the admin,
you will need to edit the config.ini file to configure authentication.
</div>


diff -r e25bdf0b0389730127acb945e286989226c7398e -r bbacba94edb55867f4722fc2c3bd3012be29bf09 web/css.php
--- a/web/css.php
+++ b/web/css.php
@@ -12,8 +12,8 @@
'css/smoothness/jquery-ui-1.7.2.custom.css',
'css/reset.css',
'mtrack.css',
+ 'css/wiki.css',
'css/tw-bootstrap.css',
- 'css/wiki.css',
'css/ticket.css',
'css/milestone.css',
'css/search.css',



https://bitbucket.org/wez/mtrack/changeset/9b955b44419d/
changeset: 9b955b44419d
user: wez
date: 2012-04-21 05:32:06
summary: improve styling for auth config page
affected #: 2 files

diff -r bbacba94edb55867f4722fc2c3bd3012be29bf09 -r 9b955b44419d9acaef99aa09e6db0fa62d9d061e web/admin/auth.php
--- a/web/admin/auth.php
+++ b/web/admin/auth.php
@@ -154,17 +154,32 @@
HTML;
}

+/* get me the right class name */
+function acc($what) {
+ if ($what == 'http' &&
+ (strlen($GLOBALS['http_configd']) +
+ strlen($GLOBALS['openid_configd']) == 0)) {
+ return 'collapse in';
+ }
+
+ return strlen($GLOBALS[$what . '_configd']) ? 'collapse in' : 'collapse';
+}

?>
-<p>
+<div class='alert alert-info'>
Select one of the following, depending
-on which one best matches your intended mtrack deployment:
-</p>
+on which one best matches your intended mtrack deployment. More options are
+possible via <tt>config.ini</tt>
+</div><form method='post'>
-<div id="authaccordion">
-<h2><a href='#'>Private (HTTP authentication)<?php echo $http_configd ?></a></h2>
-<div>
+<div id="accordion" class="accordion">
+ <div class='accordion-group'>
+ <div class='accordion-heading'>
+ <a class='accordion-toggle' data-toggle="collapse"
+ data-parent="#accordion" href="#httpauth">Private (HTTP authentication)<?php echo $http_configd ?></a>
+<div id="httpauth" class='accordion-body <?php echo acc('http'); ?>'>
+<div class='accordion-inner'><p>
I want to strictly control who has access to mtrack, and prevent
anonymous users from having any rights.
@@ -201,10 +216,11 @@
echo "</p>";
} else {
echo <<<HTML
-<p>You <em>MUST</em> add at least one user as an administrator,
+<div class='alert'>
+You <em>MUST</em> add at least one user as an administrator,
otherwise no one will be able to administer the system without editing
the config.ini file.
-</p>
+</div>
HTML;

echo <<<HTML
@@ -238,12 +254,20 @@
}
}
?>
- <input type='submit' name='setupprivate'
+ <input class='btn btn-primary' type='submit' name='setupprivate'
value='Configure Private Authentication'>

+ </div>
+ </div></div>
-<h2><a href='#'>Public (OpenID)<?php echo $openid_configd ?></a></h2>
-<div>
+</div>
+
+<div class='accordion-group'>
+ <div class='accordion-heading'>
+ <a class='accordion-toggle' data-toggle="collapse"
+ data-parent="#accordion" href="#openidauth">Public (OpenID)<?php echo $openid_configd ?></a>
+<div id="openidauth" class='accordion-body <?php echo acc('openid'); ?>'>
+<div class='accordion-inner'><p>
I want to allow the public access to mtrack, but only allow people that
I trust to make certain kinds of changes.
@@ -263,32 +287,25 @@
echo "</p>";
} else {
echo <<<HTML
-<p>You <em>MUST</em> add at least one OpenID as an administrator,
+<div class='alert'>
+You <em>MUST</em> add at least one OpenID as an administrator,
otherwise no one will be able to administer the system without editing
the config.ini file.
-</p>
+</div>
HTML;
}
?><b>Add Admin OpenID</b>: <input type="text" name="adminopenid"><br><b>Local Username</b>: <input type="text" name="adminuserid"><br>
- <input type='submit' name='setuppublic'
+ <input class='btn btn-primary' type='submit' name='setuppublic'
value='Configure Public Authentication'>
+
+ </div>
+ </div></div></div>
+
</form>
-<script>
-$(document).ready(function () {
- $('#authaccordion').accordion({
- active: <?php
- if (isset($plugins['MTrackAuth_OpenID'])) {
- echo "1";
- } else {
- echo "0";
- }
-?>});
-});
-</script><?php
mtrack_foot();



diff -r bbacba94edb55867f4722fc2c3bd3012be29bf09 -r 9b955b44419d9acaef99aa09e6db0fa62d9d061e web/css/tw-bootstrap.css
--- a/web/css/tw-bootstrap.css
+++ b/web/css/tw-bootstrap.css
@@ -1291,4 +1291,121 @@
.alert-block p + p {
margin-top: 5px;
}
+.accordion {
+ margin-bottom: 18px;
+}
+.accordion-group {
+ margin-bottom: 2px;
+ border: 1px solid #e5e5e5;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+.accordion-heading {
+ border-bottom: 0;
+}
+.accordion-heading .accordion-toggle {
+ display: block;
+ padding: 8px 15px;
+}
+.accordion-inner {
+ padding: 9px 15px;
+ border-top: 1px solid #e5e5e5;
+}
+.carousel {
+ position: relative;
+ margin-bottom: 18px;
+ line-height: 1;
+}
+.carousel-inner {
+ overflow: hidden;
+ width: 100%;
+ position: relative;
+}
+.carousel .item {
+ display: none;
+ position: relative;
+ -webkit-transition: 0.6s ease-in-out left;
+ -moz-transition: 0.6s ease-in-out left;
+ -ms-transition: 0.6s ease-in-out left;
+ -o-transition: 0.6s ease-in-out left;
+ transition: 0.6s ease-in-out left;
+}
+.carousel .item > img {
+ display: block;
+ line-height: 1;
+}
+.carousel .active,
+.carousel .next,
+.carousel .prev {
+ display: block;
+}
+.carousel .active {
+ left: 0;
+}
+.carousel .next,
+.carousel .prev {
+ position: absolute;
+ top: 0;
+ width: 100%;
+}
+.carousel .next {
+ left: 100%;
+}
+.carousel .prev {
+ left: -100%;
+}
+.carousel .next.left,
+.carousel .prev.right {
+ left: 0;
+}
+.carousel .active.left {
+ left: -100%;
+}
+.carousel .active.right {
+ left: 100%;
+}
+.carousel-control {
+ position: absolute;
+ top: 40%;
+ left: 15px;
+ width: 40px;
+ height: 40px;
+ margin-top: -20px;
+ font-size: 60px;
+ font-weight: 100;
+ line-height: 30px;
+ color: #ffffff;
+ text-align: center;
+ background: #222222;
+ border: 3px solid #ffffff;
+ -webkit-border-radius: 23px;
+ -moz-border-radius: 23px;
+ border-radius: 23px;
+ opacity: 0.5;
+ filter: alpha(opacity=50);
+}
+.carousel-control.right {
+ left: auto;
+ right: 15px;
+}
+.carousel-control:hover {
+ color: #ffffff;
+ text-decoration: none;
+ opacity: 0.9;
+ filter: alpha(opacity=90);
+}
+.carousel-caption {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ padding: 10px 15px 5px;
+ background: #333333;
+ background: rgba(0, 0, 0, 0.75);
+}
+.carousel-caption h4,
+.carousel-caption p {
+ color: #ffffff;
+}




https://bitbucket.org/wez/mtrack/changeset/e28fec55bb0f/
changeset: e28fec55bb0f
user: wez
date: 2012-04-21 08:07:17
summary: formalize cookie based login/logout. Also adopt the "proper" navbar from
twitter bootstrap so that we get the dropdown menu support.
When we have too many items, push them into a dropdown. This happens
for folks that have mtrack plugins for things like CI and so on.
affected #: 9 files

diff -r 9b955b44419d9acaef99aa09e6db0fa62d9d061e -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 inc/auth.php
--- a/inc/auth.php
+++ b/inc/auth.php
@@ -51,6 +51,13 @@
* avatar - URL to an avatar image
*/
function getUserData($username);
+
+ /** Returns true if this mechanism is one that is capable of signing
+ * out under application control */
+ function canLogOut();
+
+ /** logs the user out */
+ function LogOut();
}

class MTrackAuth
@@ -316,5 +323,21 @@
} catch (Exception $e) {
}
}
+
+ static function canLogOut() {
+ foreach (self::$mechs as $mech) {
+ if ($mech->canLogOut()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static function LogOut() {
+ foreach (self::$mechs as $mech) {
+ $mech->LogOut();
+ }
+ }
+
}



diff -r 9b955b44419d9acaef99aa09e6db0fa62d9d061e -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 inc/auth/anon.php
--- a/inc/auth/anon.php
+++ b/inc/auth/anon.php
@@ -52,5 +52,11 @@
return null;
}

+ function canLogOut() {
+ return false;
+ }
+
+ function LogOut() {
+ }
}



diff -r 9b955b44419d9acaef99aa09e6db0fa62d9d061e -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 inc/auth/http.php
--- a/inc/auth/http.php
+++ b/inc/auth/http.php
@@ -355,5 +355,12 @@
flock($fp, LOCK_UN);
$fp = null;
}
+
+ function canLogOut() {
+ return false;
+ }
+
+ function LogOut() {
+ }
}



diff -r 9b955b44419d9acaef99aa09e6db0fa62d9d061e -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 inc/auth/openid.php
--- a/inc/auth/openid.php
+++ b/inc/auth/openid.php
@@ -11,7 +11,7 @@
function augmentUserInfo(&$content) {
global $ABSWEB;
if (isset($_SESSION['openid.id'])) {
- $content .= " | <a href='{$ABSWEB}openid.php/signout'>Log off</a>";
+ //$content .= " | <a href='{$ABSWEB}openid.php/signout'>Log off</a>";
} else {
$content = "<a href='{$ABSWEB}openid.php'>Log In</a>";
}
@@ -61,6 +61,16 @@
function getUserData($username) {
return null;
}
+
+ function canLogOut() {
+ return true;
+ }
+
+ function LogOut() {
+ session_destroy();
+ header('Location: ' . $GLOBALS['ABSWEB']);
+ exit;
+ }
}




diff -r 9b955b44419d9acaef99aa09e6db0fa62d9d061e -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -141,10 +141,17 @@

if ($navbar) {
echo <<<HTML
-<div id="banner-back">
- <div id="banner">
- $projectname
- </div>
+<div class='navbar navbar-fixed-top' id='mainnav'>
+ <div class='navbar-inner'>
+ <div class='container'>
+ <a class="btn btn-navbar"
+ data-toggle="collapse" data-target=".nav-collapse">
+ <span class='icon-bar'></span>
+ <span class='icon-bar'></span>
+ <span class='icon-bar'></span>
+ </a>
+ <a class='brand' href='#'>$projectname</a>
+ <div class='nav-collapse'>
HTML;

$nav = array();
@@ -160,6 +167,7 @@
"/ticket.php/new" => array("New Ticket", 'create', 'Tickets'),
"/snippet.php" => array("Snippets", 'read', 'Snippets'),
"/admin/" => array("Administration", 'modify', 'Enumerations', 'Components', 'Projects', 'Browser'),
+ "/help.php" => array("Help", 'read', 'Wiki'),
);
foreach ($navcandidates as $url => $data) {
$label = array_shift($data);
@@ -176,16 +184,24 @@
}
}

+ echo mtrack_nav('mainnav', $nav, $userinfo);
+
echo <<<HTML
- <form id="mainsearch" action="${ABSWEB}search.php">
- $userinfo
- <input type="text" class="search" title="Search"
+ <form class='navbar-search pull-right'
+ id="mainsearch" action="${ABSWEB}search.php">
+ <input type="text" class="search-query" title="Search"
name="q" accesskey="f">
- </form>
- <div id="ajaxspin"></div>
+ </form>
+ </div>
+ <div id="ajaxspin"></div>
+
+ </div>
+ </div>
+</div>
+
+
HTML;

- echo mtrack_nav('mainnav', $nav);
}
$party_addrs = MTrackConfig::get('core', 'admin_party_remote_address');
if (MTrackConfig::get('core', 'admin_party') == 1 &&
@@ -390,7 +406,7 @@
return $html;
}

-function mtrack_nav($id, $nav) {
+function mtrack_nav($id, $nav, $userinfo) {
global $ABSWEB;

// Allow config file to manipulate the navigation bits
@@ -430,22 +446,12 @@
$where = dirname($where);
} while ($active === null && $tries++ < 100);

+ $drop = array();
+ $active_in_drop = false;
foreach ($nav as $loc => $label) {
- unset($nav[$loc]);
- $class = array();
- if (!count($elements)) {
- $class[] = "first";
- }
- if (count($nav) == 0) {
- $class[] = "last";
- }
+ $class = '';
if ($active == $loc) {
- $class[] = 'active';
- }
- if (count($class)) {
- $class = " class=\"" . implode(' ', $class) . "\"";
- } else {
- $class = '';
+ $class = ' class="active"';
}
if ($loc[0] == '/') {
$url = substr($loc, 1); // trim off leading /
@@ -455,10 +461,35 @@
if (!preg_match('/^[a-z-]+:/', $url)) {
$url = $ABSWEB . $url;
}
- $elements[] = "<li$class><a href=\"$url\">$label</a></li>";
+ if (count($elements) > 6) {
+ if (strlen($class)) $active_in_drop = true;
+ $drop[] = "<li><a href=\"$url\">$label</a></li>";
+ } else {
+ $elements[] = "<li$class><a href=\"$url\">$label</a></li>";
+ }
}
- return "<div id='$id' class='nav'><ul>" .
- implode('', $elements) . "</ul></div>";
+ if (count($drop)) {
+ $class = $active_in_drop ? ' active' : '';
+ $elements[] = "<li class='dropdown$class'>
+ <a href='#' class='dropdown-toggle' data-toggle='dropdown'
+ >More <b class='caret'></b></a><ul class='dropdown-menu'>" .
+ join("\n", $drop) . "</ul></li>";
+ }
+ if ($userinfo) {
+ $me = MTrackAuth::whoami();
+ if (MTrackAuth::canLogOut() && $me != 'anonymous') {
+ $logout = $GLOBALS['ABSWEB'] . 'logout.php';
+ $elements[] = "<li class='dropdown'>
+ <a href='#' class='dropdown-toggle' data-toggle='dropdown'
+ >$me <b class='caret'></b></a><ul class='dropdown-menu'>
+ <li>$userinfo</li>
+ <li><a href='$logout'>Log Out</a></li>
+ </ul></li>";
+ } else {
+ $elements[] = "<li>" . $userinfo . "</li>";
+ }
+ }
+ return "<ul class='nav'>" . implode('', $elements) . "</ul>";
}

function mtrack_date($tstring, $show_full = false)
@@ -1245,7 +1276,7 @@
if (php_sapi_name() != 'cli') {
set_exception_handler('mtrack_last_chance_saloon');
error_reporting(E_ALL);
- ini_set('display_errors', false);
+// ini_set('display_errors', false);
set_time_limit(300);
}



diff -r 9b955b44419d9acaef99aa09e6db0fa62d9d061e -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 web/admin/auth.php
--- a/web/admin/auth.php
+++ b/web/admin/auth.php
@@ -207,17 +207,29 @@
}
echo "<h3>Administrators</h3>";
$admins = get_admins();
+$need_pw = true;
if (count($admins)) {
echo "<p>The following users are configured with admin rights:</p>";
echo "<p>";
+ $http = MTrackAuth::getMech('MTrackAuth_HTTP');
foreach ($admins as $id) {
+ $has_pw = false;
+ if ($http && $http->readPWFile($id)) {
+ $has_pw = true;
+ $need_pw = false;
+ }
echo mtrack_username($id) . " ";
+ if (!$has_pw) {
+ echo "(no password) ";
+ }
}
echo "</p>";
-} else {
+}
+if ($need_pw) {
echo <<<HTML
<div class='alert'>
-You <em>MUST</em> add at least one user as an administrator,
+You <em>MUST</em> add at least one user as an administrator (and give
+them a password!),
otherwise no one will be able to administer the system without editing
the config.ini file.
</div>


diff -r 9b955b44419d9acaef99aa09e6db0fa62d9d061e -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 web/css/tw-bootstrap.css
--- a/web/css/tw-bootstrap.css
+++ b/web/css/tw-bootstrap.css
@@ -1409,3 +1409,720 @@
color: #ffffff;
}

+.container,
+.navbar-fixed-top .container,
+.navbar-fixed-bottom .container {
+ width: 940px;
+}
+.container {
+ margin-left: auto;
+ margin-right: auto;
+ *zoom: 1;
+}
+.container:before,
+.container:after {
+ display: table;
+ content: "";
+}
+.container:after {
+ clear: both;
+}
+.container-fluid {
+ padding-left: 20px;
+ padding-right: 20px;
+ *zoom: 1;
+}
+.container-fluid:before,
+.container-fluid:after {
+ display: table;
+ content: "";
+}
+.container-fluid:after {
+ clear: both;
+}
+.dropdown {
+ position: relative;
+}
+.dropdown-toggle {
+ *margin-bottom: -3px;
+}
+.dropdown-toggle:active,
+.open .dropdown-toggle {
+ outline: 0;
+}
+.caret {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ vertical-align: top;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid #000000;
+ opacity: 0.3;
+ filter: alpha(opacity=30);
+ content: "";
+}
+.dropdown .caret {
+ margin-top: 8px;
+ margin-left: 2px;
+}
+.dropdown:hover .caret,
+.open.dropdown .caret {
+ opacity: 1;
+ filter: alpha(opacity=100);
+}
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ float: left;
+ display: none;
+ min-width: 160px;
+ padding: 4px 0;
+ margin: 0;
+ list-style: none;
+ background-color: #ffffff;
+ border-color: #ccc;
+ border-color: rgba(0, 0, 0, 0.2);
+ border-style: solid;
+ border-width: 1px;
+ -webkit-border-radius: 0 0 5px 5px;
+ -moz-border-radius: 0 0 5px 5px;
+ border-radius: 0 0 5px 5px;
+ -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ -webkit-background-clip: padding-box;
+ -moz-background-clip: padding;
+ background-clip: padding-box;
+ *border-right-width: 2px;
+ *border-bottom-width: 2px;
+}
+.dropdown-menu.pull-right {
+ right: 0;
+ left: auto;
+}
+.dropdown-menu .divider {
+ height: 1px;
+ margin: 8px 1px;
+ overflow: hidden;
+ background-color: #e5e5e5;
+ border-bottom: 1px solid #ffffff;
+ *width: 100%;
+ *margin: -5px 0 5px;
+}
+.dropdown-menu a {
+ display: block;
+ padding: 3px 15px;
+ clear: both;
+ font-weight: normal;
+ line-height: 18px;
+ color: #333333;
+ white-space: nowrap;
+}
+.dropdown-menu li > a:hover,
+.dropdown-menu .active > a,
+.dropdown-menu .active > a:hover {
+ color: #ffffff;
+ text-decoration: none;
+ background-color: #0088cc;
+}
+.dropdown.open {
+ *z-index: 1000;
+}
+.dropdown.open .dropdown-toggle {
+ color: #ffffff;
+ background: #ccc;
+ background: rgba(0, 0, 0, 0.3);
+}
+.dropdown.open .dropdown-menu {
+ display: block;
+}
+.pull-right .dropdown-menu {
+ left: auto;
+ right: 0;
+}
+.dropup .caret,
+.navbar-fixed-bottom .dropdown .caret {
+ border-top: 0;
+ border-bottom: 4px solid #000000;
+ content: "\2191";
+}
+.dropup .dropdown-menu,
+.navbar-fixed-bottom .dropdown .dropdown-menu {
+ top: auto;
+ bottom: 100%;
+ margin-bottom: 1px;
+}
+.typeahead {
+ margin-top: 2px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+.well {
+ min-height: 20px;
+ padding: 19px;
+ margin-bottom: 20px;
+ background-color: #f5f5f5;
+ border: 1px solid #eee;
+ border: 1px solid rgba(0, 0, 0, 0.05);
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+.well blockquote {
+ border-color: #ddd;
+ border-color: rgba(0, 0, 0, 0.15);
+}
+.well-large {
+ padding: 24px;
+ -webkit-border-radius: 6px;
+ -moz-border-radius: 6px;
+ border-radius: 6px;
+}
+.well-small {
+ padding: 9px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+.nav {
+ margin-left: 0;
+ margin-bottom: 18px;
+ list-style: none;
+}
+.nav li {
+ margin-left: 0;
+}
+.nav > li > a {
+ display: block;
+}
+.nav > li > a:hover {
+ text-decoration: none;
+ background-color: #eeeeee;
+}
+.nav .nav-header {
+ display: block;
+ padding: 3px 15px;
+ font-size: 11px;
+ font-weight: bold;
+ line-height: 18px;
+ color: #999999;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ text-transform: uppercase;
+}
+.nav li + .nav-header {
+ margin-top: 9px;
+}
+.nav-list {
+ padding-left: 15px;
+ padding-right: 15px;
+ margin-bottom: 0;
+}
+.nav-list > li > a,
+.nav-list .nav-header {
+ margin-left: -15px;
+ margin-right: -15px;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+}
+.nav-list > li > a {
+ padding: 3px 15px;
+}
+.nav-list > .active > a,
+.nav-list > .active > a:hover {
+ color: #ffffff;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
+ background-color: #0088cc;
+}
+.nav-list [class^="icon-"] {
+ margin-right: 2px;
+}
+.nav-list .divider {
+ height: 1px;
+ margin: 8px 1px;
+ overflow: hidden;
+ background-color: #e5e5e5;
+ border-bottom: 1px solid #ffffff;
+ *width: 100%;
+ *margin: -5px 0 5px;
+}
+.nav-tabs,
+.nav-pills {
+ *zoom: 1;
+}
+.nav-tabs:before,
+.nav-pills:before,
+.nav-tabs:after,
+.nav-pills:after {
+ display: table;
+ content: "";
+}
+.nav-tabs:after,
+.nav-pills:after {
+ clear: both;
+}
+.nav-tabs > li,
+.nav-pills > li {
+ float: left;
+}
+.nav-tabs > li > a,
+.nav-pills > li > a {
+ padding-right: 12px;
+ padding-left: 12px;
+ margin-right: 2px;
+ line-height: 14px;
+}
+.nav-tabs {
+ border-bottom: 1px solid #ddd;
+}
+.nav-tabs > li {
+ margin-bottom: -1px;
+}
+.nav-tabs > li > a {
+ padding-top: 8px;
+ padding-bottom: 8px;
+ line-height: 18px;
+ border: 1px solid transparent;
+ -webkit-border-radius: 4px 4px 0 0;
+ -moz-border-radius: 4px 4px 0 0;
+ border-radius: 4px 4px 0 0;
+}
+.nav-tabs > li > a:hover {
+ border-color: #eeeeee #eeeeee #dddddd;
+}
+.nav-tabs > .active > a,
+.nav-tabs > .active > a:hover {
+ color: #555555;
+ background-color: #ffffff;
+ border: 1px solid #ddd;
+ border-bottom-color: transparent;
+ cursor: default;
+}
+.nav-pills > li > a {
+ padding-top: 8px;
+ padding-bottom: 8px;
+ margin-top: 2px;
+ margin-bottom: 2px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+.nav-pills > .active > a,
+.nav-pills > .active > a:hover {
+ color: #ffffff;
+ background-color: #0088cc;
+}
+.nav-stacked > li {
+ float: none;
+}
+.nav-stacked > li > a {
+ margin-right: 0;
+}
+.nav-tabs.nav-stacked {
+ border-bottom: 0;
+}
+.nav-tabs.nav-stacked > li > a {
+ border: 1px solid #ddd;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+.nav-tabs.nav-stacked > li:first-child > a {
+ -webkit-border-radius: 4px 4px 0 0;
+ -moz-border-radius: 4px 4px 0 0;
+ border-radius: 4px 4px 0 0;
+}
+.nav-tabs.nav-stacked > li:last-child > a {
+ -webkit-border-radius: 0 0 4px 4px;
+ -moz-border-radius: 0 0 4px 4px;
+ border-radius: 0 0 4px 4px;
+}
+.nav-tabs.nav-stacked > li > a:hover {
+ border-color: #ddd;
+ z-index: 2;
+}
+.nav-pills.nav-stacked > li > a {
+ margin-bottom: 3px;
+}
+.nav-pills.nav-stacked > li:last-child > a {
+ margin-bottom: 1px;
+}
+.nav-tabs .dropdown-menu,
+.nav-pills .dropdown-menu {
+ margin-top: 1px;
+ border-width: 1px;
+}
+.nav-pills .dropdown-menu {
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+.nav-tabs .dropdown-toggle .caret,
+.nav-pills .dropdown-toggle .caret {
+ border-top-color: #0088cc;
+ border-bottom-color: #0088cc;
+ margin-top: 6px;
+}
+.nav-tabs .dropdown-toggle:hover .caret,
+.nav-pills .dropdown-toggle:hover .caret {
+ border-top-color: #005580;
+ border-bottom-color: #005580;
+}
+.nav-tabs .active .dropdown-toggle .caret,
+.nav-pills .active .dropdown-toggle .caret {
+ border-top-color: #333333;
+ border-bottom-color: #333333;
+}
+.nav > .dropdown.active > a:hover {
+ color: #000000;
+ cursor: pointer;
+}
+.nav-tabs .open .dropdown-toggle,
+.nav-pills .open .dropdown-toggle,
+.nav > .open.active > a:hover {
+ color: #ffffff;
+ background-color: #999999;
+ border-color: #999999;
+}
+.nav .open .caret,
+.nav .open.active .caret,
+.nav .open a:hover .caret {
+ border-top-color: #ffffff;
+ border-bottom-color: #ffffff;
+ opacity: 1;
+ filter: alpha(opacity=100);
+}
+.navbar {
+ *position: relative;
+ *z-index: 2;
+ overflow: visible;
+ margin-bottom: 18px;
+}
+.navbar-inner {
+ padding-left: 20px;
+ padding-right: 20px;
+ background-color: #2c2c2c;
+ background-image: -moz-linear-gradient(top, #333333, #222222);
+ background-image: -ms-linear-gradient(top, #333333, #222222);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));
+ background-image: -webkit-linear-gradient(top, #333333, #222222);
+ background-image: -o-linear-gradient(top, #333333, #222222);
+ background-image: linear-gradient(top, #333333, #222222);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
+ -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
+}
+.navbar .container {
+ width: auto;
+}
+.btn-navbar {
+ display: none;
+ float: right;
+ padding: 7px 10px;
+ margin-left: 5px;
+ margin-right: 5px;
+ background-color: #2c2c2c;
+ background-image: -moz-linear-gradient(top, #333333, #222222);
+ background-image: -ms-linear-gradient(top, #333333, #222222);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));
+ background-image: -webkit-linear-gradient(top, #333333, #222222);
+ background-image: -o-linear-gradient(top, #333333, #222222);
+ background-image: linear-gradient(top, #333333, #222222);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);
+ border-color: #222222 #222222 #000000;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ filter: progid:dximagetransform.microsoft.gradient(enabled=false);
+ -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);
+ -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);
+}
+.btn-navbar:hover,
+.btn-navbar:active,
+.btn-navbar.active,
+.btn-navbar.disabled,
+.btn-navbar[disabled] {
+ background-color: #222222;
+}
+.btn-navbar:active,
+.btn-navbar.active {
+ background-color: #080808 \9;
+}
+.btn-navbar .icon-bar {
+ display: block;
+ width: 18px;
+ height: 2px;
+ background-color: #f5f5f5;
+ -webkit-border-radius: 1px;
+ -moz-border-radius: 1px;
+ border-radius: 1px;
+ -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
+ -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
+}
+.btn-navbar .icon-bar + .icon-bar {
+ margin-top: 3px;
+}
+.nav-collapse.collapse {
+ height: auto;
+}
+.navbar {
+ color: #999999;
+}
+.navbar .brand:hover {
+ text-decoration: none;
+}
+.navbar .brand {
+ float: left;
+ display: block;
+ padding: 8px 20px 12px;
+ margin-left: -20px;
+ font-size: 20px;
+ font-weight: 200;
+ line-height: 1;
+ color: #ffffff;
+}
+.navbar .navbar-text {
+ margin-bottom: 0;
+ line-height: 40px;
+}
+.navbar .btn,
+.navbar .btn-group {
+ margin-top: 5px;
+}
+.navbar .btn-group .btn {
+ margin-top: 0;
+}
+.navbar-form {
+ margin-bottom: 0;
+ *zoom: 1;
+}
+.navbar-form:before,
+.navbar-form:after {
+ display: table;
+ content: "";
+}
+.navbar-form:after {
+ clear: both;
+}
+.navbar-form input,
+.navbar-form select,
+.navbar-form .radio,
+.navbar-form .checkbox {
+ margin-top: 5px;
+}
+.navbar-form input,
+.navbar-form select {
+ display: inline-block;
+ margin-bottom: 0;
+}
+.navbar-form input[type="image"],
+.navbar-form input[type="checkbox"],
+.navbar-form input[type="radio"] {
+ margin-top: 3px;
+}
+.navbar-form .input-append,
+.navbar-form .input-prepend {
+ margin-top: 6px;
+ white-space: nowrap;
+}
+.navbar-form .input-append input,
+.navbar-form .input-prepend input {
+ margin-top: 0;
+}
+.navbar-search {
+ position: relative;
+ float: left;
+ margin-top: 6px;
+ margin-bottom: 0;
+}
+.navbar-search .search-query {
+ padding: 4px 9px;
+/* font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; */
+ font-size: 13px;
+ font-weight: normal;
+ line-height: 1;
+ color: #ffffff;
+ background-color: #626262;
+ border: 1px solid #151515;
+ -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15);
+ -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15);
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15);
+ -webkit-transition: none;
+ -moz-transition: none;
+ -ms-transition: none;
+ -o-transition: none;
+ transition: none;
+}
+.navbar-search .search-query:-moz-placeholder {
+ color: #cccccc;
+}
+.navbar-search .search-query::-webkit-input-placeholder {
+ color: #cccccc;
+}
+.navbar-search .search-query:focus,
+.navbar-search .search-query.focused {
+ padding: 5px 10px;
+ color: #333333;
+ text-shadow: 0 1px 0 #ffffff;
+ background-color: #ffffff;
+ border: 0;
+ -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
+ -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.15);
+ outline: 0;
+}
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+ position: fixed;
+ right: 0;
+ left: 0;
+ z-index: 1030;
+ margin-bottom: 0;
+}
+.navbar-fixed-top .navbar-inner,
+.navbar-fixed-bottom .navbar-inner {
+ padding-left: 0;
+ padding-right: 0;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+}
+.navbar-fixed-top .container,
+.navbar-fixed-bottom .container {
+ width: 940px;
+}
+.navbar-fixed-top {
+ top: 0;
+}
+.navbar-fixed-bottom {
+ bottom: 0;
+}
+.navbar .nav {
+ position: relative;
+ left: 0;
+ display: block;
+ float: left;
+ margin: 0 10px 0 0;
+}
+.navbar .nav.pull-right {
+ float: right;
+}
+.navbar .nav > li {
+ display: block;
+ float: left;
+}
+.navbar .nav > li > a {
+ float: none;
+ padding: 10px 10px 11px;
+ line-height: 19px;
+ color: #999999;
+ text-decoration: none;
+ text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+}
+.navbar .nav > li > a:hover {
+ background-color: transparent;
+ color: #ffffff;
+ text-decoration: none;
+}
+.navbar .nav .active > a,
+.navbar .nav .active > a:hover {
+ color: #ffffff;
+ text-decoration: none;
+ background-color: #222222;
+}
+.navbar .divider-vertical {
+ height: 40px;
+ width: 1px;
+ margin: 0 9px;
+ overflow: hidden;
+ background-color: #222222;
+ border-right: 1px solid #333333;
+}
+.navbar .nav.pull-right {
+ margin-left: 10px;
+ margin-right: 0;
+}
+.navbar .dropdown-menu {
+ margin-top: 1px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+}
+.navbar .dropdown-menu:before {
+ content: '';
+ display: inline-block;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #ccc;
+ border-bottom-color: rgba(0, 0, 0, 0.2);
+ position: absolute;
+ top: -7px;
+ left: 9px;
+}
+.navbar .dropdown-menu:after {
+ content: '';
+ display: inline-block;
+ border-left: 6px solid transparent;
+ border-right: 6px solid transparent;
+ border-bottom: 6px solid #ffffff;
+ position: absolute;
+ top: -6px;
+ left: 10px;
+}
+.navbar-fixed-bottom .dropdown-menu:before {
+ border-top: 7px solid #ccc;
+ border-top-color: rgba(0, 0, 0, 0.2);
+ border-bottom: 0;
+ bottom: -7px;
+ top: auto;
+}
+.navbar-fixed-bottom .dropdown-menu:after {
+ border-top: 6px solid #ffffff;
+ border-bottom: 0;
+ bottom: -6px;
+ top: auto;
+}
+.navbar .nav .dropdown-toggle .caret,
+.navbar .nav .open.dropdown .caret {
+ border-top-color: #ffffff;
+ border-bottom-color: #ffffff;
+}
+.navbar .nav .active .caret {
+ opacity: 1;
+ filter: alpha(opacity=100);
+}
+.navbar .nav .open > .dropdown-toggle,
+.navbar .nav .active > .dropdown-toggle,
+.navbar .nav .open.active > .dropdown-toggle {
+ background-color: transparent;
+}
+.navbar .nav .active > .dropdown-toggle:hover {
+ color: #ffffff;
+}
+.navbar .nav.pull-right .dropdown-menu,
+.navbar .nav .dropdown-menu.pull-right {
+ left: auto;
+ right: 0;
+}
+.navbar .nav.pull-right .dropdown-menu:before,
+.navbar .nav .dropdown-menu.pull-right:before {
+ left: auto;
+ right: 12px;
+}
+.navbar .nav.pull-right .dropdown-menu:after,
+.navbar .nav .dropdown-menu.pull-right:after {
+ left: auto;
+ right: 13px;
+}
+


diff -r 9b955b44419d9acaef99aa09e6db0fa62d9d061e -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 web/logout.php
--- /dev/null
+++ b/web/logout.php
@@ -0,0 +1,4 @@
+<?php # vim:ts=2:sw=2:et:
+/* For licensing and copyright terms, see the file named LICENSE */
+include '../inc/common.php';
+MTrackAuth::LogOut();


diff -r 9b955b44419d9acaef99aa09e6db0fa62d9d061e -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 web/mtrack.css
--- a/web/mtrack.css
+++ b/web/mtrack.css
@@ -1,87 +1,3 @@
-/* {{{ Navigation bar */
-.nav h2, .nav hr {
- display: none;
-}
-
-.nav ul {
- font-size: 9pt;
- list-style: none;
- margin: 0;
- text-align: right;
-}
-.nav li {
- border-right: 1px solid #d7d7d7;
- display: inline;
- padding: 0 0.75em;
- white-space: nowrap;
-}
-.nav li.last {
- border-right: none;
-}
-
-#mainnav {
- font-size: 9pt;
- display: block;
- line-height: 3.3em;
- margin-left: 1em;
- overflow: hidden;
- height: 3.3em;
-}
-#mainnav ul {
- overflow: hidden;
-}
-
-#mainsearch input {
- background-color: #444;
- background-color: rgba(255, 255, 255, 0.3);
- color: white;
- font-size: 10pt;
- border: 1px solid #111;
-}
-#mainsearch input:hover {
- background-color: #bfbfbf;
- background-color: rgba(255, 255, 255, 0.5);
- color: white;
-}
-
-#mainsearch input:focus {
- background-color: white;
- color: #444;
-}
-#mainsearch input::-webkit-input-placeholder {
- color: #eee !important;
-}
-#mainsearch input:-moz-placeholder {
- color: #eee !important;
-}
-
-#mainnav li {
- border: none;
- padding: 0px;
- margin: 0px;
- display: block;
- float: left;
-}
-#mainnav li a {
- color: #bfbfbf;
- padding: 1em;
- text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
-}
-
-#mainnav :link, #mainnav :visited {
-}
-
-#mainnav li.active {
- background-color: #000;
-}
-
-#mainnav :link:hover, #mainnav :visited:hover,
-#mainnav .active :link, #mainnav .active :visited {
- color: white;
- text-decoration: none;
-}
-
-/* --- }}} */
body {
margin-top: 2em;
margin-bottom: 0px;
@@ -246,33 +162,6 @@
h2.reportgroup {
}

-#banner-back {
- position: fixed;
- padding: 0em 1em 0em 1em;
- left: 0px;
- top: 0px;
- right: 0px;
- z-index: 20;
- height: 3em;
- min-width: 940px;
-
- background-color: #222222;
- background-repeat: repeat-x;
- background-image: -khtml-gradient(linear, left top, left bottom, from(#333333), to(#222222));
- background-image: -moz-linear-gradient(top, #333333, #222222);
- background-image: -ms-linear-gradient(top, #333333, #222222);
- background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #333333), color-stop(100%, #222222));
- background-image: -webkit-linear-gradient(top, #333333, #222222);
- background-image: -o-linear-gradient(top, #333333, #222222);
- background-image: linear-gradient(top, #333333, #222222);
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);
- -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
- -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
-
- color: white;
-}
-
/* due to the fixed headers/footers, we need to offset the position
* of named anchors in the page if we want links to work correctly */
a[name] {
@@ -281,7 +170,7 @@
font-size: 1.5em;
}

-#banner {
+.brand {
display: inline;
font-family: Calibri, Arial, Verdana, 'Bitstream Vera Sans',
Helvetica, sans-serif;
@@ -297,13 +186,18 @@
margin-top: 2.25em;
}

+#mainnav div.container {
+ width: auto;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+
#mainsearch {
- float: right;
margin: 0;
line-height: 3em;
}

-#mainsearch input.search {
+#mainsearch input.search-query {
width: 10em;
border-radius: 4px;
-webkit-border-radius: 4px;



https://bitbucket.org/wez/mtrack/changeset/a7efa53d6bed/
changeset: a7efa53d6bed
user: wez
date: 2012-04-21 08:24:47
summary: nicer errors for wiki page and admin-party
affected #: 4 files

diff -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 -r a7efa53d6bed53e5a2df60672bdcad5ec2bae36b inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -549,7 +549,7 @@
}
}
}
- throw new Exception("unable to make temp dir based on path $candidate");
+ throw new Exception("unable to make temp dir based on path $candidate. Check permissions and ownership on vardir and ensure that it is writable to the webserver process");
}

function mtrack_diff_strings($before, $now)
@@ -1276,7 +1276,7 @@
if (php_sapi_name() != 'cli') {
set_exception_handler('mtrack_last_chance_saloon');
error_reporting(E_ALL);
-// ini_set('display_errors', false);
+ ini_set('display_errors', false);
set_time_limit(300);
}



diff -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 -r a7efa53d6bed53e5a2df60672bdcad5ec2bae36b web/css/wiki.css
--- a/web/css/wiki.css
+++ b/web/css/wiki.css
@@ -1,5 +1,12 @@
/* Styles for wiki related markup */

+div#wiki-error {
+ position: fixed;
+ left: 1em;
+ right: 1em;
+ top: 3.5em;
+}
+
a.wikipmark {
margin-left: 0.25em;
color: #eee !important;


diff -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 -r a7efa53d6bed53e5a2df60672bdcad5ec2bae36b web/mtrack.css
--- a/web/mtrack.css
+++ b/web/mtrack.css
@@ -192,6 +192,13 @@
padding-right: 1em;
}

+#mainnav + div.alert {
+ margin-top: 4em;
+ margin-left: 1em;
+ margin-right: 1em;
+ z-index: 100;
+}
+
#mainsearch {
margin: 0;
line-height: 3em;


diff -r e28fec55bb0f9f86da8807c2a775360f17cb8d53 -r a7efa53d6bed53e5a2df60672bdcad5ec2bae36b web/wiki.php
--- a/web/wiki.php
+++ b/web/wiki.php
@@ -171,9 +171,8 @@
<button class='btn'
onclick="document.location.href = '${ABSWEB}help.php'; return false;"
><i class='icon-book'></i> Help &amp; Page List</button>
- <div class='ui-state-error ui-corner-all' id='wiki-error'
- style="display:none">
- <span class='ui-icon ui-icon-alert'></span>
+ <div class='alert alert-danger' id='wiki-error' style="display:none">
+ <a class='close' data-dismiss='alert'>&times;</a><span id="wiki-error-text">Whoops</span></div><br>



https://bitbucket.org/wez/mtrack/changeset/49e7cfeb97eb/
changeset: 49e7cfeb97eb
user: wez
date: 2012-04-21 08:34:25
summary: fix "X"'s in modals and alerts
affected #: 4 files

diff -r a7efa53d6bed53e5a2df60672bdcad5ec2bae36b -r 49e7cfeb97ebca0d01d45a860721fb3f5bcfe441 inc/watch.php
--- a/inc/watch.php
+++ b/inc/watch.php
@@ -46,7 +46,7 @@
var V = $val;
$('#watcher-$object-$id').click(function () {
var dlg = $('<div class="modal hide fade"/>');
- dlg.append('<div class="modal-header"><a class="close" data-dismiss="modal">x</a><h3>Watching</h3></div>');
+ dlg.append('<div class="modal-header"><a class="close" data-dismiss="modal">&times;</a><h3>Watching</h3></div>');
var frm = $('<form/>');
var body = $('<div class="modal-body"/>');
var tbl = $('<table/>');


diff -r a7efa53d6bed53e5a2df60672bdcad5ec2bae36b -r 49e7cfeb97ebca0d01d45a860721fb3f5bcfe441 web/browse.php
--- a/web/browse.php
+++ b/web/browse.php
@@ -241,7 +241,7 @@
echo <<<HTML
<div id='forkdialog' class='modal hide fade'><div class='modal-header'>
- <a class='close' data-dismiss='modal'>x</a>
+ <a class='close' data-dismiss='modal'>&times;</a><h3>Fork a repo</h3></div><form id='forkform'
@@ -288,7 +288,7 @@
echo <<<HTML
<div id='deletedialog' class='modal hide fade'><div class='modal-header'>
- <a class='close' data-dismiss='modal'>x</a>
+ <a class='close' data-dismiss='modal'>&times;</a><h3>Really delete this Repo?</h3></div><form id='deleteform' action='${ABSWEB}admin/deleterepo.php'
@@ -335,7 +335,7 @@
echo <<<HTML
<div id='newdialog' class='modal hide fade'><div class='modal-header'>
- <a class='close' data-dismiss='modal'>x</a>
+ <a class='close' data-dismiss='modal'>&times;</a><h3>Create a new Repo</h3></div><form id='newrepoform' action='${ABSWEB}admin/repo.php/new'


diff -r a7efa53d6bed53e5a2df60672bdcad5ec2bae36b -r 49e7cfeb97ebca0d01d45a860721fb3f5bcfe441 web/plan.php
--- a/web/plan.php
+++ b/web/plan.php
@@ -75,7 +75,7 @@

<div id='tktdialog' class='modal hide'><div class='modal-header'>
- <a class='close' data-dismiss='modal'>x</a>
+ <a class='close' data-dismiss='modal'>&times;</a><h3><span id='tkttitle'></span><span id='tktsummary'></span></h3>


diff -r a7efa53d6bed53e5a2df60672bdcad5ec2bae36b -r 49e7cfeb97ebca0d01d45a860721fb3f5bcfe441 web/query.php
--- a/web/query.php
+++ b/web/query.php
@@ -105,7 +105,7 @@
<br><div id='colselector' class='modal hide fade'><div class='modal-header'>
- <a class='close' data-dismiss='modal'>x</a>
+ <a class='close' data-dismiss='modal'>&times;</a><h3>Choose Columns, drag to re-order</h3></div><div class='modal-body'>



https://bitbucket.org/wez/mtrack/changeset/807f3b70beea/
changeset: 807f3b70beea
user: wez
date: 2012-04-21 16:21:36
summary: really fix the default wiki repo path.
affected #: 1 file

diff -r 49e7cfeb97ebca0d01d45a860721fb3f5bcfe441 -r 807f3b70beea2cf2d7752b9e64710f78e63b33fd bin/init.php
--- a/bin/init.php
+++ b/bin/init.php
@@ -476,7 +476,7 @@
$r = new MTrackRepo;
$r->shortname = 'wiki';
$r->scmtype = $wiki_repo_type;
- $r->repopath = realpath($vardir) . DIRECTORY_SEPARATOR . 'default/wiki';
+ $r->repopath = realpath($vardir) . DIRECTORY_SEPARATOR . 'repos/default/wiki';
$r->description = 'The mtrack wiki pages are stored here';
echo " ** Creating repo 'wiki' of type $r->scmtype to hold Wiki content at $r->repopath\n";
echo " ** (use --repo option to specify an alternate location)\n";



https://bitbucket.org/wez/mtrack/changeset/7d5d19db45e7/
changeset: 7d5d19db45e7
user: wez
date: 2012-04-21 16:22:18
summary: add option to run indexer after each change in the context of the
modifying request.

This makes it easier to check things on a development system.
affected #: 4 files

diff -r 807f3b70beea2cf2d7752b9e64710f78e63b33fd -r 7d5d19db45e771b1ad63543279ba13e755fe5d41 config.ini.sample
--- a/config.ini.sample
+++ b/config.ini.sample
@@ -40,6 +40,13 @@
; MTrackSearchEngineLucene or MTrackSearchEngineSolr
;search_engine = MTrackSearchEngineLucene

+; Whether the search engine is triggered via cron.
+; if set to true, search index is updated as part of the modifying
+; action, increasing overall latency for writes/updates.
+; It is STRONGLY recommended that you leave this at false and use the
+; cron job.
+update_search_immediate = false
+
[repos]
; If true, permit the creation and forking of per-user repositories
allow_user_repo_creation = true


diff -r 807f3b70beea2cf2d7752b9e64710f78e63b33fd -r 7d5d19db45e771b1ad63543279ba13e755fe5d41 inc/changeset.php
--- a/inc/changeset.php
+++ b/inc/changeset.php
@@ -74,6 +74,9 @@
$db = MTrackDB::get();
$db->commit();
}
+ if (MTrackConfig::get('core', 'update_search_immediate')) {
+ MTrackSearchDB::index_object($this->object);
+ }
}

/* loads a ticket and associates it with a changeset.


diff -r 807f3b70beea2cf2d7752b9e64710f78e63b33fd -r 7d5d19db45e771b1ad63543279ba13e755fe5d41 inc/search.php
--- a/inc/search.php
+++ b/inc/search.php
@@ -72,7 +72,17 @@

if (isset(self::$funcs[$key])) {
$func = self::$funcs[$key];
- return call_user_func($func, $id);
+ /* some of the indexing code is verbose; if we're updating
+ * inline as part of the requests, we need to turn that off
+ * to avoid breaking the page output! */
+ if (MTrackConfig::get('core', 'update_search_immediate')) {
+ ob_start();
+ }
+ $ret = call_user_func($func, $id);
+ if (MTrackConfig::get('core', 'update_search_immediate')) {
+ ob_end_clean();
+ }
+ return $ret;
}
return false;
}


diff -r 807f3b70beea2cf2d7752b9e64710f78e63b33fd -r 7d5d19db45e771b1ad63543279ba13e755fe5d41 inc/search/lucene.php
--- a/inc/search/lucene.php
+++ b/inc/search/lucene.php
@@ -607,6 +607,9 @@
$p = MTrackConfig::get('core', 'searchdb');
if (!is_dir($p)) {
$idx = Zend_Search_Lucene::create($p);
+ if (!is_dir($rp)) {
+ throw new Exception("unable to initialize search db in '$p', check permissions and ensure that the web server user is able to create files and directories in its parent");
+ }
chmod($p, 0777);
} else {
$idx = Zend_Search_Lucene::open($p);



https://bitbucket.org/wez/mtrack/changeset/a9029dd52f57/
changeset: a9029dd52f57
user: wez
date: 2012-04-21 17:15:41
summary: add live searching to the search box
affected #: 3 files

diff -r 7d5d19db45e771b1ad63543279ba13e755fe5d41 -r a9029dd52f570deb07b40bdb89cc5ea86fb93ea2 inc/search.php
--- a/inc/search.php
+++ b/inc/search.php
@@ -107,6 +107,38 @@
return self::getEngine()->search($query);
}

+ static function rest_query_array($method, $uri, $captures) {
+ $res = self::rest_query($method, $uri, $captures);
+ /* aggregate the ticket search results */
+ $q = MTrackAPI::getParam('q');
+ $res = $res->results;
+ if (preg_match("/^[#0-9 -]+$/", $q)) {
+ $t = MTrackAPI::invoke('GET', "/ticket/search/basic", null, array(
+ 'q' => trim($q)));
+ foreach ($t->result as $r) {
+ $o = new stdclass;
+ $o->link = "#$r->nsident $r->summary";
+ $o->url = $GLOBALS['ABSWEB'] . "ticket.php/$r->nsident";
+ $res[] = $o;
+ }
+ if (count($res) == 0 && strpos($q, ' ')) {
+ /* smells like a ticket list */
+ $o = new stdclass;
+ $o->link = "<em>Show a ticket list containing $q</em>";
+ $o->url = $GLOBALS['ABSWEB'] . "search.php?q=" . urlencode($q);
+ $res[] = $o;
+ }
+ }
+ /* catch all: take them to the main search page.
+ * This is here because there are some quick search cases we don't
+ * handle here, and they might want to see the help on searching */
+ $o = new stdclass;
+ $o->link = "<em>Search for $q</em>";
+ $o->url = $GLOBALS['ABSWEB'] . "search.php?q=" . urlencode($q);
+ $res[] = $o;
+ return $res;
+ }
+
static function rest_query($method, $uri, $captures) {
MTrackAPI::checkAllowed($method, 'GET');
$q = MTrackAPI::getParam('q');
@@ -323,4 +355,5 @@
}

MTrackAPI::register('/search/query', 'MTrackSearchDB::rest_query');
+MTrackAPI::register('/search/query/array', 'MTrackSearchDB::rest_query_array');



diff -r 7d5d19db45e771b1ad63543279ba13e755fe5d41 -r a9029dd52f570deb07b40bdb89cc5ea86fb93ea2 web/js/mtrack.js
--- a/web/js/mtrack.js
+++ b/web/js/mtrack.js
@@ -348,6 +348,17 @@
}
mtrack_markitup($("textarea.wiki"), true);

+ $('#mainsearch input.search-query').marcoPolo({
+ url: ABSWEB + 'api.php/search/query/array',
+ formatItem: function(data) {
+ console.log("format", data);
+ return data.link;
+ },
+ onSelect: function(data, $item) {
+ console.log("selected", data);
+ window.location = data.url;
+ }
+ });

$.tablesorter.addParser({
id: 'ticket',


diff -r 7d5d19db45e771b1ad63543279ba13e755fe5d41 -r a9029dd52f570deb07b40bdb89cc5ea86fb93ea2 web/mtrack.css
--- a/web/mtrack.css
+++ b/web/mtrack.css
@@ -210,9 +210,6 @@
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
}
-#mainsearch a {
- color: white;
-}

::-webkit-input-placeholder {
color: #999 !important;



https://bitbucket.org/wez/mtrack/changeset/23c5d2909e23/
changeset: 23c5d2909e23
user: wez
date: 2012-04-21 17:18:35
summary: make the fallback ticket printing nicer in the live search
affected #: 1 file

diff -r a9029dd52f570deb07b40bdb89cc5ea86fb93ea2 -r 23c5d2909e23d1e35b255796cd44fa4235ac0f46 inc/search.php
--- a/inc/search.php
+++ b/inc/search.php
@@ -117,11 +117,11 @@
'q' => trim($q)));
foreach ($t->result as $r) {
$o = new stdclass;
- $o->link = "#$r->nsident $r->summary";
$o->url = $GLOBALS['ABSWEB'] . "ticket.php/$r->nsident";
+ $o->link = "<a class='ticketlink' href='$o->url'>#$r->nsident $r->summary</a>";
$res[] = $o;
}
- if (count($res) == 0 && strpos($q, ' ')) {
+ if (count($res) == 0 && preg_match("/[ -]/", $q)) {
/* smells like a ticket list */
$o = new stdclass;
$o->link = "<em>Show a ticket list containing $q</em>";



https://bitbucket.org/wez/mtrack/changeset/4dc5d2eaf019/
changeset: 4dc5d2eaf019
user: wez
date: 2012-04-21 19:01:13
summary: how about deleting dead wiki pages from the index?
affected #: 9 files

diff -r 23c5d2909e23d1e35b255796cd44fa4235ac0f46 -r 4dc5d2eaf0192820c3d710df70a71d0a179f2ea5 inc/cache.php
--- a/inc/cache.php
+++ b/inc/cache.php
@@ -23,6 +23,17 @@
}
}

+function mtrack_cache_blow_all()
+{
+ $cachedir = MTrackConfig::get('core', 'vardir') . '/cmdcache';
+ foreach (scandir($cachedir) as $name) {
+ $filename = "$cachedir/$name";
+ if (is_file($filename)) {
+ unlink($filename);
+ }
+ }
+}
+
/* walks the cache; loads each element and examines the keys.
* if the key prefix matches $key, that element is removed */
function mtrack_cache_blow_matching($key)


diff -r 23c5d2909e23d1e35b255796cd44fa4235ac0f46 -r 4dc5d2eaf0192820c3d710df70a71d0a179f2ea5 inc/changeset.php
--- a/inc/changeset.php
+++ b/inc/changeset.php
@@ -74,7 +74,10 @@
$db = MTrackDB::get();
$db->commit();
}
- if (MTrackConfig::get('core', 'update_search_immediate')) {
+ /* if in immediate mode, index the object, but not if it is a wiki page
+ * as that is handled in post-commit */
+ if (!preg_match("/^wiki:/", $this->object) &&
+ MTrackConfig::get('core', 'update_search_immediate')) {
MTrackSearchDB::index_object($this->object);
}
}


diff -r 23c5d2909e23d1e35b255796cd44fa4235ac0f46 -r 4dc5d2eaf0192820c3d710df70a71d0a179f2ea5 inc/issue.php
--- a/inc/issue.php
+++ b/inc/issue.php
@@ -755,6 +755,10 @@
MTrackSearchDB::add("ticket:$ident:comment:$id", array(
'description' => $text,
'stored:date' => $when,
+ /* need the status in here so that we can cheaply exclude
+ * closed tickets. Otherwise they keep popping up in the
+ * results */
+ 'status' => $i->status,
'who' => $who,
), true);



diff -r 23c5d2909e23d1e35b255796cd44fa4235ac0f46 -r 4dc5d2eaf0192820c3d710df70a71d0a179f2ea5 inc/search.php
--- a/inc/search.php
+++ b/inc/search.php
@@ -22,6 +22,7 @@
interface IMTrackSearchEngine {
public function setBatchMode();
public function commit($optimize = false);
+ public function remove($object);
public function add($object, $fields, $replace = false);
/** returns an array of MTrackSearchResult objects corresponding
* to matches to the supplied query string */
@@ -99,6 +100,10 @@
self::getEngine()->commit($optimize);
}

+ static function remove($object) {
+ self::getEngine()->remove($object);
+ }
+
static function add($object, $fields, $replace = false) {
self::getEngine()->add($object, $fields, $replace);
}
@@ -108,9 +113,21 @@
}

static function rest_query_array($method, $uri, $captures) {
- $res = self::rest_query($method, $uri, $captures);
+ $q = MTrackAPI::getParam('q');
+
+ /* full text. We hide closed tickets to reduce noise;
+ * we're more likely to be searching for active items here */
+ MTrackAPI::checkAllowed($method, 'GET');
+ $res = mtrack_cache(array('MTrackSearchDB', '_do_search'),
+ array("$q -status:closed"), 6);
+
+ foreach ($res->results as $idx => $group) {
+ if (!MTrackACL::hasAnyRights($group->object, 'read')) {
+ unset($res->results[$idx]);
+ }
+ }
+
/* aggregate the ticket search results */
- $q = MTrackAPI::getParam('q');
$res = $res->results;
if (preg_match("/^[#0-9 -]+$/", $q)) {
$t = MTrackAPI::invoke('GET', "/ticket/search/basic", null, array(
@@ -143,7 +160,7 @@
MTrackAPI::checkAllowed($method, 'GET');
$q = MTrackAPI::getParam('q');
$results = mtrack_cache(array('MTrackSearchDB', '_do_search'),
- array($q), 60);
+ array($q), 6);

foreach ($results->results as $idx => $group) {
if (!MTrackACL::hasAnyRights($group->object, 'read')) {


diff -r 23c5d2909e23d1e35b255796cd44fa4235ac0f46 -r 4dc5d2eaf0192820c3d710df70a71d0a179f2ea5 inc/search/lucene.php
--- a/inc/search/lucene.php
+++ b/inc/search/lucene.php
@@ -607,7 +607,7 @@
$p = MTrackConfig::get('core', 'searchdb');
if (!is_dir($p)) {
$idx = Zend_Search_Lucene::create($p);
- if (!is_dir($rp)) {
+ if (!is_dir($p)) {
throw new Exception("unable to initialize search db in '$p', check permissions and ensure that the web server user is able to create files and directories in its parent");
}
chmod($p, 0777);
@@ -635,14 +635,25 @@
$this->idx = null;
}

- public function add($object, $fields, $replace = false)
+ public function remove($object)
{
$idx = $this->getIdx();

+ foreach ($idx->find("object:\"$object\"") as $hit) {
+ $res = $idx->delete($hit->id);
+ }
+ $idx->commit();
+ }
+
+ public function add($object, $fields, $replace = false)
+ {
+ echo "lucene: add($object)\n";
+
+ $idx = $this->getIdx();
+
if ($replace) {
- $term = new Zend_Search_Lucene_Index_Term($object, 'object');
- foreach ($idx->termDocs($term) as $id) {
- $idx->delete($id);
+ foreach ($idx->find("object:\"$object\"") as $hit) {
+ $idx->delete($hit->id);
}
}

@@ -672,6 +683,9 @@
$hits = $idx->find($q);
$result = array();
foreach ($hits as $hit) {
+ if ($idx->isDeleted($hit->id)) {
+ continue;
+ }
$r = new MTrackSearchResultLucene;
$r->_query = $q;
$r->objectid = $hit->object;


diff -r 23c5d2909e23d1e35b255796cd44fa4235ac0f46 -r 4dc5d2eaf0192820c3d710df70a71d0a179f2ea5 inc/search/solr.php
--- a/inc/search/solr.php
+++ b/inc/search/solr.php
@@ -42,6 +42,12 @@
$this->post('<optimize/>');
}

+ public function remove($object) {
+ $id = $this->nsprefix . $object;
+ $xml = "<delete><id>$id</id></delete>";
+ $this->post($xml);
+ }
+
public function add($object, $fields, $replace = false) {
$id = $this->nsprefix . $object;
$xml = "<add overwrite='true'><doc><field name='id'>$id</field>";


diff -r 23c5d2909e23d1e35b255796cd44fa4235ac0f46 -r 4dc5d2eaf0192820c3d710df70a71d0a179f2ea5 inc/wiki-item.php
--- a/inc/wiki-item.php
+++ b/inc/wiki-item.php
@@ -108,15 +108,20 @@
static function index_item($object)
{
list($ignore, $ident) = explode(':', $object, 2);
+
$w = MTrackWikiItem::loadByPageName($ident);
+ if ($w && strlen($w->content)) {
+ MTrackSearchDB::add("wiki:$w->pagename", array(
+ 'type' => 'wiki',
+ 'wiki' => $w->content,
+ 'name' => $w->pagename,
+ 'who' => $w->who,
+ ), true);
+ } else {
+ MTrackSearchDB::remove($object);
+ }
+ }

- MTrackSearchDB::add("wiki:$w->pagename", array(
- 'type' => 'wiki',
- 'wiki' => $w->content,
- 'name' => $w->pagename,
- 'who' => $w->who,
- ), true);
- }
static function _get_parent_for_acl($objectid) {
if (preg_match("/^(wiki:.*)\/([^\/]+)$/", $objectid, $M)) {
return $M[1];
@@ -561,6 +566,28 @@
/* is this affecting the wiki? */
if ($repo->getBrowseRootName() == 'default/wiki') {
mtrack_cache_blow(array('MTrackWikiItem', '_build_tree_top'), array());
+
+ /* this is also an ideal time to update the search index for wiki
+ * pages, if we are set to immediate updates. Normal objects that
+ * live solely in our database wouldn't need such special treatment,
+ * but repo changes are made partially in the context of the commit
+ * hook and so won't have the same view consistency as the rest
+ * of the system */
+ if (MTrackConfig::get('core', 'update_search_immediate')) {
+ $suf = MTrackConfig::get('core', 'wikifilenamesuffix');
+ $len = strlen($suf);
+ foreach ($files as $name) {
+ $name = substr($name, strlen($repo->shortname) + 1);
+ $is_wiki = $len == 0;
+ if ($len && substr($name, -$len) == $suf) {
+ $name = substr($name, 0, strlen($name) - $len);
+ $is_wiki = true;
+ }
+ if ($is_wiki) {
+ MTrackSearchDB::index_object("wiki:$name");
+ }
+ }
+ }
}
return true;
}


diff -r 23c5d2909e23d1e35b255796cd44fa4235ac0f46 -r 4dc5d2eaf0192820c3d710df70a71d0a179f2ea5 t/rest/search.php
--- a/t/rest/search.php
+++ b/t/rest/search.php
@@ -1,7 +1,7 @@
<?php # vim:ts=2:sw=2:et:
require getenv('INCUB_ROOT') . '/inc/Test/init.php';

-plan(14);
+plan(17);

function update_index()
{
@@ -78,3 +78,22 @@
$excerpt = $body->results[0]->hits[0]->excerpt;
like($excerpt, ",<span class='hl'>stuff</span>,", "stuff");

+// Validate that deleting a wiki page also makes it go away from index
+list($st, $h, $base) = rest_api_func('POST', "/wiki/page/SearchTest",
+ null, null,
+ array(
+ 'content' => "" # deleted
+ ));
+
+is($st, 200, "delete a wiki page");
+update_index();
+mtrack_cache_blow_all();
+
+list($st, $h, $body) = rest_api_func('GET', '/search/query', array(
+ 'q' => 'stuff'
+));
+is($st, 200, "query for stuff");
+if (!is(count($body->results), 0, "no hits")) {
+ var_dump($body);
+}
+


diff -r 23c5d2909e23d1e35b255796cd44fa4235ac0f46 -r 4dc5d2eaf0192820c3d710df70a71d0a179f2ea5 web/admin/logs.php
--- a/web/admin/logs.php
+++ b/web/admin/logs.php
@@ -53,7 +53,7 @@
}
?><form method='post'>
- <button type='submit' name='reset'
+ <button type='submit' name='reset' class='btn btn-danger'
>Rebuild Index from scratch on next run</button></form><?php



https://bitbucket.org/wez/mtrack/changeset/6506962cf1ba/
changeset: 6506962cf1ba
user: wez
date: 2012-04-21 20:23:36
summary: some tweaks with the live search
affected #: 3 files

diff -r 4dc5d2eaf0192820c3d710df70a71d0a179f2ea5 -r 6506962cf1babe736e6c2ac7a39fec7d593e69ed inc/issue.php
--- a/inc/issue.php
+++ b/inc/issue.php
@@ -1583,7 +1583,7 @@
/* we only want to return matches that have no children of their own */

if (preg_match("/^[a-z0-9A-Z]+$/", $id)) {
- foreach (MTrackDB::q("select tid, nsident, summary, (select count(k.tid) from tickets k where k.ptid = t.tid) as kids from tickets t where nsident like '$id%' and kids = 0")->fetchAll(PDO::FETCH_OBJ)
+ foreach (MTrackDB::q("select tid, nsident, summary, (select count(k.tid) from tickets k where k.ptid = t.tid) as kids from tickets t where nsident like '$id%' and kids = 0 and status != 'closed'")->fetchAll(PDO::FETCH_OBJ)
as $obj) {
$res[$obj->tid] = $obj;
}
@@ -1629,7 +1629,8 @@
}
if (preg_match("/^[a-z0-9A-Z]+$/", $id)) {
foreach (MTrackDB::q("select tid, nsident, summary from tickets
- where nsident like '$id%'")->fetchAll(PDO::FETCH_OBJ)
+ where nsident like '$id%' and status != 'closed'")
+ ->fetchAll(PDO::FETCH_OBJ)
as $obj) {
$res[$obj->tid] = $obj;
}


diff -r 4dc5d2eaf0192820c3d710df70a71d0a179f2ea5 -r 6506962cf1babe736e6c2ac7a39fec7d593e69ed inc/search.php
--- a/inc/search.php
+++ b/inc/search.php
@@ -118,8 +118,9 @@
/* full text. We hide closed tickets to reduce noise;
* we're more likely to be searching for active items here */
MTrackAPI::checkAllowed($method, 'GET');
+ $notickets = "$q -status:closed";
$res = mtrack_cache(array('MTrackSearchDB', '_do_search'),
- array("$q -status:closed"), 6);
+ array($notickets), 6);

foreach ($res->results as $idx => $group) {
if (!MTrackACL::hasAnyRights($group->object, 'read')) {
@@ -129,16 +130,27 @@

/* aggregate the ticket search results */
$res = $res->results;
+ $truncated = false;
+
+ if (count($res) > 8) {
+ $res = array_slice($res, 0, 8);
+ $truncated = true;
+ }
+
if (preg_match("/^[#0-9 -]+$/", $q)) {
$t = MTrackAPI::invoke('GET', "/ticket/search/basic", null, array(
'q' => trim($q)));
foreach ($t->result as $r) {
+ if (count($res) > 8) {
+ $truncated = true;
+ break;
+ }
$o = new stdclass;
$o->url = $GLOBALS['ABSWEB'] . "ticket.php/$r->nsident";
$o->link = "<a class='ticketlink' href='$o->url'>#$r->nsident $r->summary</a>";
$res[] = $o;
}
- if (count($res) == 0 && preg_match("/[ -]/", $q)) {
+ if (preg_match("/[ -]/", $q)) {
/* smells like a ticket list */
$o = new stdclass;
$o->link = "<em>Show a ticket list containing $q</em>";
@@ -150,7 +162,8 @@
* This is here because there are some quick search cases we don't
* handle here, and they might want to see the help on searching */
$o = new stdclass;
- $o->link = "<em>Search for $q</em>";
+ $o->link = $truncated ? "<em>More results for $q</em>" :
+ "<em>Search for $q</em>";
$o->url = $GLOBALS['ABSWEB'] . "search.php?q=" . urlencode($q);
$res[] = $o;
return $res;


diff -r 4dc5d2eaf0192820c3d710df70a71d0a179f2ea5 -r 6506962cf1babe736e6c2ac7a39fec7d593e69ed web/mtrack.css
--- a/web/mtrack.css
+++ b/web/mtrack.css
@@ -211,6 +211,10 @@
-moz-border-radius: 4px;
}

+#mainsearch ol.mp_list {
+ width: 20em !important;
+}
+
::-webkit-input-placeholder {
color: #999 !important;
}



https://bitbucket.org/wez/mtrack/changeset/95d06accdb6e/
changeset: 95d06accdb6e
user: wez
date: 2012-04-21 20:39:16
summary: improve presentation of the admin index area
affected #: 1 file

diff -r 6506962cf1babe736e6c2ac7a39fec7d593e69ed -r 95d06accdb6ecb10aa08b01818f429a5a2933e18 web/admin/index.php
--- a/web/admin/index.php
+++ b/web/admin/index.php
@@ -24,46 +24,54 @@
}

if (MTrackACL::hasAnyRights('Projects', 'modify')) {
- add_cat("<a href='{$ABSWEB}admin/project.php'>Projects</a> and their notification settings", 'projects');
+ add_cat("<a href='{$ABSWEB}admin/project.php'>Projects and their notification settings</a>", 'projects');
}

if (MTrackACL::hasAnyRights('Enumerations', 'modify')) {
$eurl = $ABSWEB . 'admin/enum.php';
- add_cat("<a href='$eurl/Priority'>Priority</a>, <a href='$eurl/TicketState'>TicketState</a>, <a href='$eurl/Severity'>Severity</a>, <a href='$eurl/Resolution'>Resolution</a> and <a href='$eurl/Classification'>Classification</a> fields used in tickets", 'tickets');
+ add_cat("<a href='$eurl/Priority'>Priority</a>", 'tickets');
+ add_cat("<a href='$eurl/TicketState'>TicketState</a>", 'tickets');
+ add_cat("<a href='$eurl/Severity'>Severity</a>", 'tickets');
+ add_cat("<a href='$eurl/Resolution'>Resolution</a>", 'tickets');
+ add_cat("<a href='$eurl/Classification'>Classification</a>", 'tickets');
add_cat("<a href='{$ABSWEB}admin/customfield.php'>Custom Fields</a>", 'tickets');
}

if (MTrackACL::hasAnyRights('Components', 'modify')) {
- add_cat("<a href='{$ABSWEB}admin/component.php'>Components</a> and their associations with Projects", 'tickets', 'projects');
+ add_cat("<a href='{$ABSWEB}admin/component.php'>Components</a>", 'tickets', 'projects');
}

if (MTrackACL::hasAnyRights('Tickets', 'create')) {
- add_cat("<a href='{$ABSWEB}admin/importcsv.php'>Import Tickets</a> from a CSV file", 'tickets');
+ add_cat("<a href='{$ABSWEB}admin/importcsv.php'>Import Tickets from a CSV file</a>", 'tickets');
}

if (MTrackACL::hasAnyRights('Browser', 'modify')) {
- add_cat("Configure <a href='{$ABSWEB}admin/repo.php'>Repositories</a> and their links to Projects", 'repo');
+ add_cat("<a href='{$ABSWEB}admin/repo.php'>Repositories and their links to Projects</a>", 'repo');
}

if (MTrackACL::hasAllRights('User', 'modify')) {
- add_cat("Administer <a href='{$ABSWEB}admin/auth.php'>Authentication</a>", 'user');
- add_cat("Administer <a href='{$ABSWEB}admin/user.php'>Users</a>", 'user');
+ add_cat("<a href='{$ABSWEB}admin/auth.php'>Authentication</a>", 'user');
+ add_cat("<a href='{$ABSWEB}admin/user.php'>Users</a>", 'user');
}

if (MTrackACL::hasAllRights('Browser', 'modify')) {
add_cat("<a href='{$ABSWEB}admin/logs.php'>Indexer logs</a>", 'logs');
}

+echo "<div class='well'>";
+echo "<ul class='nav nav-list'>\n";
foreach ($cat_titles as $cat => $title) {
$links = $by_cat[$cat];
if (count($links) == 0) {
continue;
}
- echo "<h2>$title</h2>";
+ echo "<li class='nav-header'>$title</li>";
foreach ($links as $link) {
- echo $link, "<br>\n";
+ echo "<li>$link</li>\n";
}
}
+echo "</ul>";
+echo "</div>";

mtrack_foot();




https://bitbucket.org/wez/mtrack/changeset/79fc46674542/
changeset: 79fc46674542
user: wez
date: 2012-04-21 22:38:48
summary: Add simple and non-ugly looking projects page
affected #: 3 files

diff -r 95d06accdb6ecb10aa08b01818f429a5a2933e18 -r 79fc4667454227964d9d506f1aee3b3721c7c397 inc/issue.php
--- a/inc/issue.php
+++ b/inc/issue.php
@@ -360,6 +360,88 @@
$reason = preg_replace('/\[(\d+)\]/', "[$this->shortname\$1]", $reason);
return $reason;
}
+
+ static function rest_project_apply($in, MTrackProject $P, MTrackChangeset $CS)
+ {
+ error_log(json_encode($in));
+ if (isset($in->name)) {
+ $P->name = trim($in->name);
+ }
+ if (isset($in->shortname)) {
+ $P->shortname = trim($in->shortname);
+ }
+ if (isset($in->ordinal)) {
+ $P->ordinal = (int)$in->ordinal;
+ }
+ if (isset($in->notifyemail)) {
+ $P->notifyemail = $in->notifyemail;
+ }
+
+ // TODO: component association
+ }
+
+ static function rest_project_return(MTrackProject $P)
+ {
+ $p = MTrackAPI::makeObj($P, 'projid');
+
+ // TODO: component association
+
+ return $p;
+ }
+
+ static function rest_project_new($method, $uri, $captures)
+ {
+ MTrackAPI::checkAllowed($method, 'GET', 'POST');
+ if ($method == 'GET') {
+ MTrackACL::requireAllRights('Projects', 'read');
+
+ return MTrackDB::q(
+ 'select projid as id, name, shortname, ordinal, notifyemail
+ from projects order by ordinal')->fetchAll(PDO::FETCH_OBJ);
+ }
+ MTrackACL::requireAllRights('Projects', 'create');
+
+ $in = MTrackAPI::getPayload();
+
+ $CS = MTrackChangeset::begin("project:X", "Added project $in->name");
+ $P = new MTrackProject;
+ self::rest_project_apply($in, $P, $CS);
+
+ $P->save($CS);
+ $CS->setObject("project:$P->projid");
+ $CS->commit();
+
+ return self::rest_project_return($P);
+ }
+
+ static function rest_project($method, $uri, $captures)
+ {
+ MTrackAPI::checkAllowed($method, 'GET', 'PUT');
+
+ $pid = $captures['p'];
+ if (preg_match('/^\d+$/', $pid)) {
+ $P = self::loadById($pid);
+ } else {
+ $P = self::loadByName($pid);
+ }
+ if (!$P) {
+ MTrackAPI::error(404, "no such project", $pid);
+ }
+ if ($method == 'PUT') {
+ MTrackACL::requireAllRights("project:$P->projid", 'modify');
+ $in = MTrackAPI::getPayload();
+ if (!is_object($in)) {
+ MTrackAPI::error(400, "expected json payload");
+ }
+ $CS = MTrackChangeset::begin("project:$P->projid",
+ "Edit project $in->name");
+ self::rest_project_apply($in, $P, $CS);
+ $P->save($CS);
+ $CS->commit();
+ }
+ MTrackACL::requireAllRights("project:$P->projid", 'read');
+ return self::rest_project_return($P);
+ }
}

/* The listener protocol is to return true if all is good,
@@ -2391,3 +2473,7 @@
MTrackLink::register('comment', 'MTrackIssue::resolve_comment_link');
MTrackLink::register('component', 'MTrackComponent::resolve_link');

+
+MTrackAPI::register('/project', 'MTrackProject::rest_project_new');
+MTrackAPI::register('/project/:p', 'MTrackProject::rest_project');
+


diff -r 95d06accdb6ecb10aa08b01818f429a5a2933e18 -r 79fc4667454227964d9d506f1aee3b3721c7c397 web/admin/project.php
--- a/web/admin/project.php
+++ b/web/admin/project.php
@@ -4,211 +4,141 @@

MTrackACL::requireAnyRights('Projects', 'modify');

-if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- if (isset($_POST['cancel'])) {
- header("Location: ${ABSWEB}admin/");
- exit;
- }
-
- $pid = $_GET['edit'];
- if ($pid == 'new') {
- MTrackACL::requireAnyRights('Projects', 'create');
- $P = new MTrackProject;
- } else {
- $P = MTrackProject::loadById($pid);
- if (!$P) {
- throw new Exception("invalid project " . htmlentities($pid));
- }
- MTrackACL::requireAnyRights("project:$pid", 'modify');
- }
-
- $P->name = $_POST["name"];
- $P->shortname = $_POST["shortname"];
- $P->ordinal = $_POST["ordinal"];
- $P->notifyemail = $_POST["email"];
- $CS = MTrackChangeset::begin("project:X",
- $pid == 'new' ?
- "added project $P->name" :
- "edit project $P->name");
- $P->save($CS);
-
- if (MTrackACL::hasAnyRights('Components', 'modify')) {
- MTrackDB::q('delete from components_by_project where projid = ?', $P->projid);
- if (isset($_POST['components'])) {
- $comps = $_POST['components'];
- foreach ($comps as $cid) {
- MTrackDB::q(
- 'insert into components_by_project (compid, projid) values (?, ?)',
- $cid, $P->projid);
- }
- }
- }
-
- $CS->setObject("project:$P->projid");
- if (isset($_POST['perms'])) {
- MTrackACL::setACL("project:$P->projid", 0, json_decode($_POST['perms']));
- }
- $CS->commit();
-
- header("Location: ${ABSWEB}admin/project.php");
- exit;
-}
-
mtrack_head("Administration - Projects");

-?>
+$projects = json_encode(MTrackAPI::invoke('GET', '/project/')->result);
+
+echo <<<HTML
<h1>Projects</h1><p>
Projects can be created to track development on a per-project or per-product
basis. Components may be associated with a project, as well as a default
email distribution address.
</p>
-<?php
+<script>
+$(document).ready(function() {
+ var projects = new MTrackProjectCollection($projects);

-if (isset($_GET['edit'])) {
- $pid = $_GET['edit'];
- if ($pid != 'new') {
- $q = MTrackDB::q('select * from projects where projid = ?', $pid);
- $p = null;
- foreach ($q as $row) {
- $p = $row;
- }
- if ($p == null) {
- throw new Exception("no such project " . htmlentities($pid));
- }
- } else {
- $p = array(
- 'projid' => 'new',
- 'name' => 'My New Project',
- 'shortname' => 'newproject',
- 'ordinal' => 5,
- 'notifyemail' => null
- );
- }
- echo "<form method='post' action=\"{$ABSWEB}admin/project.php?edit=$pid\">";
+// $('#projlist').sortable();

- echo "<table>";
- $name = htmlentities($p['name'], ENT_QUOTES, 'utf-8');
- $sname = htmlentities($p['shortname'], ENT_QUOTES, 'utf-8');
- $ord = htmlentities($p['ordinal'], ENT_QUOTES, 'utf-8');
- $email = htmlentities($p['notifyemail'], ENT_QUOTES, 'utf-8');
- echo "<tr><th>Name</th>",
- "<td><input type='text' name='name' value='$name'></td></tr>";
- echo "<tr><th>Short Name</th>",
- "<td><input type='text' name='shortname' value='$sname'></td></tr>";
- echo "<tr><th>Sorting</th>",
- "<td><input type='text' name='ordinal' value='$ord'></td></tr>";
- echo "<tr><th>Group Email Address</th>",
- "<td><input type='text' name='email' value='$email'></td></tr>";
- echo "</table>";
+ function show_edit(P) {
+ var dlg = $('#editdialog');
+ $('input[name=shortname]', dlg).val(P.get('shortname'));
+ $('input[name=name]', dlg).val(P.get('name'));
+ $('input[name=notifyemail]', dlg).val(P.get('notifyemail'));

- if (MTrackACL::hasAnyRights('Components', 'modify')) {
- $components = array();
- foreach (MTrackDB::q(
- 'select compid, name, deleted from components order by name')
- ->fetchAll() as $row) {
- if ($row[2]) {
- $row[1] .= " (deleted)";
- }
- $components[$row[0]] = $row[1];
- }
- $p_by_c = array();
- if ($pid != 'new') {
- foreach (MTrackDB::q(
- 'select compid from components_by_project where projid = ?', $pid)
- ->fetchAll() as $row) {
- $p_by_c[$row[0]] = $row[0];
- }
- }
- echo "<h2>Components</h2>";
- echo "<p>Associate component(s) with this project</p>";
- echo mtrack_multi_select_box('components', "(select to add)",
- $components, $p_by_c);
+ var is_new = P.isNew();
+
+ dlg.modal('show');
+
+ $('#saveproj').on('click.saveproj', function () {
+ P.save({
+ shortname: $('input[name=shortname]', dlg).val(),
+ name: $('input[name=name]', dlg).val(),
+ notifyemail: $('input[name=notifyemail]', dlg).val()
+ },{
+ success: function (model, resp) {
+ dlg.modal('hide');
+ if (is_new) {
+ projects.add(model, {at: projects.length});
+ } else {
+ render_list();
+ }
+ },
+ error: function (model, resp) {
+ var err;
+ if (!_.isObject(resp)) {
+ err = resp;
+ } else {
+ err = resp.statusText;
+ try {
+ var r = JSON.parse(resp.responseText);
+ err = r.message;
+ } catch (e) {
+ }
+ }
+ $('<div class="alert alert-danger">' +
+ "<a class='close' data-dismiss='alert'>&times;</a>" +
+ err + '</div>').
+ appendTo('#editdialog div.modal-body');
+ }
+ });
+ });
+ };
+
+ $('#projlist').on('click', 'a', function () {
+ var P = $(this.parentElement).data('project');
+ show_edit(P);
+ });
+
+ function addOne(P) {
+ var li = $('<li/>');
+ var a = $('<a href="#"/>');
+ a.text(P.get('name'));
+ li.append(a);
+ li.data('project', P);
+ li.appendTo('#projlist');
}

- $repos = array();
- foreach (MTrackDB::q('select distinct r.repoid, shortname from project_repo_link p left join repos r on p.repoid = r.repoid where projid = ?', (int)$pid) as $row) {
- $repos[$row[0]] = $row[1];
+ function render_list() {
+ $('#projlist').empty();
+ projects.each(function (P) {
+ addOne(P);
+ });
}
- foreach (MTrackDB::q("select repoid, shortname from repos where parent = 'project:' || ?", $p['shortname']) as $row) {
- $repos[$row[0]] = $row[1];
- }
+ render_list();

- if ($pid != 'new') {
- echo "<h2>Groups</h2>";
- echo "<p>The following groups are associated with this project. You may assign permissions to groups to make it easier to manage groups of users.</p>";
+ projects.bind('add', function (P) {
+ addOne(P);
+ });

- foreach (MTrackDB::q('select name from groups where project = ?', $pid)
- as $row) {
- echo "<a href='{$ABSWEB}admin/group.php?pid=$pid&amp;group=$row[0]'>"
- . htmlentities($row[0], ENT_QUOTES, 'utf-8') . '</a><br>';
- }
+ $('#newrepobtn').click(function () {
+ var P = new MTrackProject;
+ show_edit(P);
+ });

- echo "<a class='button' href=\"{$ABSWEB}admin/group.php?pid=$pid\">New Group</a>";
- }
+ // Clear any text from the form when it hides
+ $('#editdialog').on('hidden', function () {
+ $('input', this).val('');
+ $('div.alert', this).remove();
+ $('#saveproj').off('click.saveproj');
+ });
+});
+</script>
+<button id='newrepobtn' class='btn btn-primary'
+ type='button'><i class='icon-plus icon-white'></i> New Project</button>
+<div id='editdialog' class='modal hide'>
+ <div class='modal-header'>
+ <a class='close' data-dismiss='modal'>&times;</a>
+ <h3>Edit Project</h3>
+ </div>
+ <div class='modal-body'>
+ <p>
+ Short name:<br>
+ <input type='text' name='shortname'>
+ </p>
+ <p>
+ Descriptive name:<br>
+ <input type='text' name='name'>
+ </p>
+ <p>
+ Send Notification Email to:<br>
+ <input type='text' name='notifyemail'>
+ </p>
+ </div>
+ <div class='modal-footer'>
+ <button class='btn' data-dismiss='modal'>Cancel</button>
+ <button id='saveproj'
+ class='btn btn-primary'>Save Project</button>
+ </div>
+ </form>
+</div>

- echo "<h2>Linked Repositories</h2>";
- if (count($repos)) {
- echo "<ul>\n";
- foreach ($repos as $rid => $name) {
- echo "<li><a href=\"{$ABSWEB}admin/repo.php/$rid\">" .
- htmlentities($name, ENT_QUOTES, 'utf-8') . "</a></li>\n";
- }
- echo "</ul>\n";
- } else {
- echo "<i>No linked repositories</i>\n";
- }
- echo "<br><br>\n";
+<ul id='projlist' class='nav nav-pills nav-stacked'>
+</ul>

- if (MTrackACL::hasAnyRights("project:$pid", 'modify')) {
- $action_map = array(
- 'Admin' => array(
- 'modify' => 'Administer via web UI',
- ),
- );
+HTML;

- MTrackACL::renderACLForm('perms', "project:$pid", $action_map);
- }
-
- echo "<button type='submit'>Save</button>";
- echo "<button type='submit' name='cancel'>Cancel</button>";
-
- echo "</form>";
-} else {
-?>
-<p>
-Select a project below to edit it, or click the "Add" button to create
-a new project.
-</p>
-<?php
-
- echo "<table>\n";
- foreach (MTrackDB::q(
- 'select projid, name, shortname, ordinal, notifyemail
- from projects order by ordinal') as $row) {
-
- $pid = $row[0];
- $name = htmlentities($row[1], ENT_QUOTES, 'utf-8');
- $sname = htmlentities($row[2], ENT_QUOTES, 'utf-8');
- if ($sname != $name) {
- $sname = " ($sname)";
- } else {
- $sname = '';
- }
- $email = htmlentities($row[4], ENT_QUOTES, 'utf-8');
-
- echo "<tr>",
- "<td><a href=\"{$ABSWEB}admin/project.php?edit=$pid\">$name$sname</a></td>",
- "<td>$email</td>",
- "</tr>\n";
-
- }
- echo "</table><br>";
-
- echo "<form method='get' action=\"{$ABSWEB}admin/project.php\">";
- echo "<input type='hidden' name='edit' value='new'>";
- echo "<button type='submit'>Add Project</button></form>";
-}

mtrack_foot();



diff -r 95d06accdb6ecb10aa08b01818f429a5a2933e18 -r 79fc4667454227964d9d506f1aee3b3721c7c397 web/js/models.js
--- a/web/js/models.js
+++ b/web/js/models.js
@@ -25,6 +25,22 @@
return nestedCollection;
}

+var MTrackProject = Backbone.Model.extend({
+ url: function() {
+ if (this.isNew()) {
+ return ABSWEB + "api.php/project";
+ }
+ return ABSWEB + "api.php/project/" + this.id;
+ }
+});
+
+var MTrackProjectCollection = Backbone.Collection.extend({
+ model: MTrackProject,
+ url: function() {
+ return ABSWEB + 'api.php/project';
+ }
+});
+
var MTrackTicketChange = Backbone.Model.extend({
});




https://bitbucket.org/wez/mtrack/changeset/2be198161521/
changeset: 2be198161521
user: wez
date: 2012-04-22 01:19:23
summary: administering to ticket enums is no longer a travesty of UI/UX.

Uses sortables with nice drag-n-drop styling (borrowed from the planning
page) and backbone models to make for a reasonably nice experience.
affected #: 2 files

diff -r 79fc4667454227964d9d506f1aee3b3721c7c397 -r 2be19816152164e9ea820d3687731c9953572bb0 inc/issue.php
--- a/inc/issue.php
+++ b/inc/issue.php
@@ -75,6 +75,90 @@
$old, $this->value);

}
+
+ /* new-or-list */
+ function do_rest_list($method, $uri, $captures)
+ {
+ MTrackAPI::checkAllowed($method, 'GET');
+ MTrackACL::requireAllRights('Enumerations', 'read');
+
+ $res = array();
+ foreach ($this->enumerate(true) as $item) {
+ $o = new stdclass;
+ $o->id = $item['name'];
+ $o->value = $item['value'];
+ $o->deleted = $item['deleted'];
+ $res[] = $o;
+ }
+ return $res;
+ }
+
+ function do_rest_item($method, $uri, $captures)
+ {
+ MTrackAPI::checkAllowed($method, 'GET', 'PUT');
+
+ $id = $captures['s'];
+ $cls = get_class($this);
+
+ try {
+ $E = new $cls($id);
+ } catch (Exception $e) {
+ if ($method == 'GET') {
+ MTrackAPI::error(404, "no such such item", $id);
+ }
+ /* we're creating */
+ MTrackACL::requireAllRights('Enumerations', 'create');
+ $E = new $cls;
+ }
+
+ if ($method == 'PUT') {
+ MTrackACL::requireAllRights('Enumerations', 'modify');
+ $in = MTrackAPI::getPayload();
+ if (!is_object($in)) {
+ MTrackAPI::error(400, "expected json payload");
+ }
+
+ if ($E->name) {
+ $CS = MTrackChangeset::begin("enum:$this->tablename:$in->id",
+ "Edit $this->tablename $in->name");
+ } else {
+ $CS = MTrackChangeset::begin("enum:$this->tablename:$in->id",
+ "Added $this->tablename $in->name");
+ }
+
+ if (isset($in->id)) {
+ $newname = trim($in->id);
+ if (!strlen($newname)) {
+ throw new Exception("invalid or missing id");
+ }
+ if ($E->name && $newname != $E->name) {
+ /* would need to modify all kinds of data in the tables
+ * in a way that is not visible to the change audit
+ * tables */
+ throw new Exception("renaming is not currently supported");
+ }
+ $E->name = $newname;
+ }
+
+ if (isset($in->value)) {
+ $E->value = $in->value;
+ }
+ if (isset($in->deleted)) {
+ $E->deleted = $in->deleted;
+ }
+
+ $E->save($CS);
+ $CS->commit();
+ }
+ MTrackACL::requireAllRights("Enumerations", 'read');
+
+ $o = new stdclass;
+ $o->id = $E->name;
+ $o->value = $E->value;
+ $o->deleted = $E->deleted;
+
+ return $o;
+ }
}

class MTrackTicketState extends MTrackEnumeration {
@@ -82,6 +166,15 @@
protected $fieldname = 'statename';
protected $fieldvalue = 'ordinal';

+ static function rest_list($method, $uri, $captures) {
+ $o = new self;
+ return $o->do_rest_list($method, $uri, $captures);
+ }
+ static function rest_item($method, $uri, $captures) {
+ $o = new self;
+ return $o->do_rest_item($method, $uri, $captures);
+ }
+
static function loadByName($name) {
return new MTrackTicketState($name);
}
@@ -114,6 +207,15 @@
protected $fieldname = 'priorityname';
protected $fieldvalue = 'value';

+ static function rest_list($method, $uri, $captures) {
+ $o = new self;
+ return $o->do_rest_list($method, $uri, $captures);
+ }
+ static function rest_item($method, $uri, $captures) {
+ $o = new self;
+ return $o->do_rest_item($method, $uri, $captures);
+ }
+
static function loadByName($name) {
return new MTrackPriority($name);
}
@@ -124,6 +226,16 @@
protected $fieldname = 'sevname';
protected $fieldvalue = 'ordinal';

+ static function rest_list($method, $uri, $captures) {
+ $o = new self;
+ return $o->do_rest_list($method, $uri, $captures);
+ }
+ static function rest_item($method, $uri, $captures) {
+ $o = new self;
+ return $o->do_rest_item($method, $uri, $captures);
+ }
+
+
static function loadByName($name) {
return new MTrackSeverity($name);
}
@@ -134,6 +246,16 @@
protected $fieldname = 'resname';
protected $fieldvalue = 'ordinal';

+ static function rest_list($method, $uri, $captures) {
+ $o = new self;
+ return $o->do_rest_list($method, $uri, $captures);
+ }
+ static function rest_item($method, $uri, $captures) {
+ $o = new self;
+ return $o->do_rest_item($method, $uri, $captures);
+ }
+
+
static function loadByName($name) {
return new MTrackResolution($name);
}
@@ -144,6 +266,16 @@
protected $fieldname = 'classname';
protected $fieldvalue = 'ordinal';

+ static function rest_list($method, $uri, $captures) {
+ $o = new self;
+ return $o->do_rest_list($method, $uri, $captures);
+ }
+ static function rest_item($method, $uri, $captures) {
+ $o = new self;
+ return $o->do_rest_item($method, $uri, $captures);
+ }
+
+
static function loadByName($name) {
return new MTrackClassification($name);
}
@@ -2477,3 +2609,18 @@
MTrackAPI::register('/project', 'MTrackProject::rest_project_new');
MTrackAPI::register('/project/:p', 'MTrackProject::rest_project');

+MTrackAPI::register('/ticket/enums/state', 'MTrackTicketState::rest_list');
+MTrackAPI::register('/ticket/enums/state/:s', 'MTrackTicketState::rest_item');
+
+MTrackAPI::register('/ticket/enums/priority', 'MTrackPriority::rest_list');
+MTrackAPI::register('/ticket/enums/priority/:s', 'MTrackPriority::rest_item');
+
+MTrackAPI::register('/ticket/enums/severity', 'MTrackSeverity::rest_list');
+MTrackAPI::register('/ticket/enums/severity/:s', 'MTrackSeverity::rest_item');
+
+MTrackAPI::register('/ticket/enums/resolution', 'MTrackResolution::rest_list');
+MTrackAPI::register('/ticket/enums/resolution/:s', 'MTrackResolution::rest_item');
+
+MTrackAPI::register('/ticket/enums/classification', 'MTrackClassification::rest_list');
+MTrackAPI::register('/ticket/enums/classification/:s', 'MTrackClassification::rest_item');
+


diff -r 79fc4667454227964d9d506f1aee3b3721c7c397 -r 2be19816152164e9ea820d3687731c9953572bb0 web/admin/enum.php
--- a/web/admin/enum.php
+++ b/web/admin/enum.php
@@ -5,88 +5,142 @@
MTrackACL::requireAnyRights('Enumerations', 'modify');

$ename = mtrack_get_pathinfo();
-$enums = array('Priority', 'TicketState', 'Severity', 'Resolution', 'Classification');
+$enums = array(
+ 'Priority' => '/ticket/enums/priority',
+ 'TicketState' => '/ticket/enums/state',
+ 'Severity' => '/ticket/enums/severity',
+ 'Resolution' => '/ticket/enums/resolution',
+ 'Classification' => '/ticket/enums/classification',
+);

-if (!in_array($ename, $enums)) {
+if (!isset($enums[$ename])) {
throw new Exception("Invalid enum type");
}

-if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- $cls = 'MTrack' . $ename;
- if (isset($_POST["$ename:name:"]) && strlen($_POST["$ename:name:"])) {
- $obj = new $cls;
- $obj->name = $_POST["$ename:name:"];
- $obj->value = $_POST["$ename:value:"];
- $CS = MTrackChangeset::begin("enum:$obj->tablename:$obj->name",
- "added $ename $obj->name");
- $obj->save($CS);
- $CS->commit();
- }
-
- foreach ($_POST as $name => $value) {
- if (preg_match("/^$ename:value:(.+)$/", $name, $M)) {
- $n = $M[1];
- try {
- $obj = new $cls($n);
- } catch (Exception $e) {
- continue;
- }
- $changed = false;
-
- if ($obj->value != $value) {
- $obj->value = $value;
- $changed = true;
- }
- if (isset($_POST["$ename:deleted:$n"]) &&
- $_POST["$ename:deleted:$n"] == "on") {
- $deleted = '1';
- } else {
- $deleted = '';
- }
- if ($obj->deleted != $deleted) {
- $obj->deleted = $deleted;
- $changed = true;
- }
-
- if ($changed) {
- $CS = MTrackChangeset::begin("enum:$obj->tablename:$obj->name",
- "changed $ename $obj->name");
- $obj->save($CS);
- $CS->commit();
- }
- }
- }
- header("Location: ${ABSWEB}admin/");
- exit;
-}
-
mtrack_head("Administration - $ename");

-echo "<form method='post'>";
+$url = $enums[$ename];
+$col = json_encode(MTrackAPI::invoke('GET', $url)->result);

-$cls = 'MTrack' . $ename;
-$obj = new $cls;
-echo "<br><b>$ename values</b><br>\n";
-$vals = $obj->enumerate(true);
-echo "<table><tr><th>Name</th><th>Value</th><th>Deleted</th></tr>\n";
-foreach ($vals as $V) {
- $n = htmlentities($V['name'], ENT_QUOTES, 'utf-8');
- $v = htmlentities($V['value'], ENT_QUOTES, 'utf-8');
- $del = $V['deleted'] ? ' checked="checked" ' : '';
- echo "<tr>" .
- "<td>$n</td>" .
- "<td><input type='text' name='$ename:value:$n' value='$v'></td>" .
- "<td><input type='checkbox' name='$ename:deleted:$n' $del></td>" .
- "</tr>\n";
+echo <<<HTML
+<h1>Administer Ticket $ename</h1>
+<p>Drag to re-order!</p>
+<script>
+$(document).ready(function() {
+ var MODEL = Backbone.Model.extend({
+ url: function() {
+ return ABSWEB + "api.php$url/" + this.id;
+ }
+ });
+ var ECOL = Backbone.Collection.extend({
+ model: MODEL
+ });
+ var COL = new ECOL($col);
+
+ function capture_error(model, resp) {
+ var err;
+ if (!_.isObject(resp)) {
+ err = resp;
+ } else {
+ err = resp.statusText;
+ try {
+ var r = JSON.parse(resp.responseText);
+ err = r.message;
+ } catch (e) {
+ }
+ }
+ $('<div class="alert alert-danger">' +
+ "<a class='close' data-dismiss='alert'>&times;</a>" +
+ "<b>" + model.id + "</b>: " + err + '</div>').
+ appendTo('#content');
+ }
+
+ $('#elist').sortable({
+ placeholder: 'ticketDragTarget',
+ start: function (evt, ui) {
+ ui.placeholder.height(ui.item.height());
+ ui.helper.addClass('draggingTicket');
+ },
+ stop: function (evt, ui) {
+ ui.item.removeClass('draggingTicket');
+ },
+ update: function (evt, ui) {
+ /* update the value of each model in the collection */
+ $('#elist').children().each(function (idx, elt) {
+ var E = $(elt).data('model');
+ idx++; // want it to be 1 based
+ if (E.get('value') != idx) {
+ E.save({value: idx}, {error: capture_error});
+ }
+ });
+ }
+ });
+ $('#elist').on('click', 'input[type=checkbox]', function () {
+ var E = $(this).closest('li').data('model');
+ E.set({deleted: $(this).prop('checked')});
+ E.save();
+ });
+
+ function addOne(E) {
+ var li = $('<li/>');
+ var span = $('<div class="handle"/>');
+ span.text(E.id);
+ li.append(span);
+ li.data('model', E);
+ var cont = $('<div class="mark"/>');
+ var cb = $('<input type="checkbox">');
+ cb.attr('checked', E.get('deleted'));
+ cont.append(cb);
+ cont.append(" <span>Mark as deleted</span>");
+ li.append(cont);
+ li.appendTo('#elist');
+ }
+
+ COL.each(function (E) {
+ addOne(E);
+ });
+
+ $('#newbtn').click(function () {
+ var input = $('#newname');
+ var name = input.val();
+ input.val('');
+ if (COL.get(name)) {
+ return;
+ }
+ var E = new MODEL;
+ E.save({
+ id: name,
+ value: COL.length,
+ deleted: false
+ }, {
+ success: function (model, resp) {
+ COL.add(model, {at: COL.length});
+ addOne(model);
+ },
+ error: capture_error
+ });
+ });
+});
+</script>
+<style>
+ul.tickets {
+padding-top: 1em;
}
-echo "<tr>" .
- "<td><input type='text' name='$ename:name:' value=''></td>" .
- "<td><input type='text' name='$ename:value:' value=''></td>" .
- "<td>Add a new $ename</td>" .
- "</tr>\n";
-echo "</table>\n";
+ul.tickets li div.mark {
+ font-size: smaller;
+ float: right;
+}
+input#newname {
+ font-size: 1.4em;
+ width: 20em;
+}
+</style>
+<ul id='elist' class='tickets'></ul>

-echo "<button>Save Changes</button></form>";
+<input type="text" id="newname" placeholder="Add a new $ename">
+<button id='newbtn' class='btn btn-primary'><i class='icon-white icon-plus'></i> Add</button>
+HTML;
+

mtrack_foot();




https://bitbucket.org/wez/mtrack/changeset/967ad07ad3a1/
changeset: 967ad07ad3a1
user: wez
date: 2012-04-22 01:59:33
summary: add admin navigation across the majority of admin pages.
It's almost looking like someone designed this now.
affected #: 10 files

diff -r 2be19816152164e9ea820d3687731c9953572bb0 -r 967ad07ad3a17edc2a042912c7f5688a76e1691c inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -1118,6 +1118,94 @@
return $html;
}

+function _mtrack_admin_nav_add_cat(&$by_cat, $url) {
+ $cats = func_get_args();
+ array_shift($cats);
+ foreach ($cats as $cat) {
+ $by_cat[$cat][] = $url;
+ }
+}
+
+
+function mtrack_admin_nav()
+{
+ global $ABSWEB;
+
+ $cat_titles = array(
+ 'tickets' => 'Configure Tickets',
+ 'projects' => 'Configure Projects &amp; Notifications',
+ 'repo' => 'Configure Repositories',
+ 'user' => 'User Administration &amp; Authentication',
+ 'logs' => 'Review Logs',
+ 'import' => 'Import',
+ );
+
+ $by_cat = array();
+ $add_cat = '_mtrack_admin_nav_add_cat';
+
+ if (MTrackACL::hasAnyRights('Projects', 'modify')) {
+ $add_cat($by_cat, "<a href='{$ABSWEB}admin/project.php'><i class='icon-envelope'></i> Projects and their notification settings</a>", 'projects');
+ }
+
+ if (MTrackACL::hasAnyRights('Enumerations', 'modify')) {
+ $eurl = $ABSWEB . 'admin/enum.php';
+ $add_cat($by_cat, "<a href='$eurl/Priority'><i class='icon-list'></i> Priority</a>", 'tickets');
+ $add_cat($by_cat, "<a href='$eurl/TicketState'><i class='icon-list'></i> TicketState</a>", 'tickets');
+ $add_cat($by_cat, "<a href='$eurl/Severity'><i class='icon-list'></i> Severity</a>", 'tickets');
+ $add_cat($by_cat, "<a href='$eurl/Resolution'><i class='icon-list'></i> Resolution</a>", 'tickets');
+ $add_cat($by_cat, "<a href='$eurl/Classification'><i class='icon-list'></i> Classification</a>", 'tickets');
+ $add_cat($by_cat, "<a href='{$ABSWEB}admin/customfield.php'><i class='icon-list'></i> Custom Fields</a>", 'tickets');
+ }
+
+ if (MTrackACL::hasAnyRights('Components', 'modify')) {
+ $add_cat($by_cat, "<a href='{$ABSWEB}admin/component.php'><i class='icon-list'></i> Components</a>", 'projects');
+ }
+
+ if (MTrackACL::hasAnyRights('Browser', 'modify')) {
+ $add_cat($by_cat, "<a href='{$ABSWEB}admin/repo.php'><i class='icon-file'></i> Repositories and their links to Projects</a>", 'repo');
+ }
+
+ if (MTrackACL::hasAllRights('User', 'modify')) {
+ $add_cat($by_cat, "<a href='{$ABSWEB}admin/auth.php'>Authentication</a>", 'user');
+ $add_cat($by_cat, "<a href='{$ABSWEB}admin/user.php'><i class='icon-user'></i> Users</a>", 'user');
+ }
+
+ if (MTrackACL::hasAnyRights('Tickets', 'create')) {
+ $add_cat($by_cat, "<a class='btn' href='{$ABSWEB}admin/importcsv.php'><i class='icon-upload'></i> Import Tickets from a CSV file</a>", 'import');
+ }
+
+ if (MTrackACL::hasAllRights('Browser', 'modify')) {
+ $add_cat($by_cat, "<a href='{$ABSWEB}admin/logs.php'><i class='icon-cog'></i> Indexer logs</a>", 'logs');
+ }
+
+ /* there should be an easier way to figure this out, but there's
+ * no guaranteed way with PHP */
+ $here = preg_replace('{^.*/admin/}', '', $_SERVER['REQUEST_URI']);
+
+ echo "<div class='well' id='adminnav'>";
+ echo "<ul class='nav nav-list'>\n";
+ foreach ($cat_titles as $cat => $title) {
+ $links = $by_cat[$cat];
+ if (count($links) == 0) {
+ continue;
+ }
+ echo "<li class='nav-header'>$title</li>";
+ foreach ($links as $link) {
+ $class = '';
+ if (preg_match("{href='.*/admin/(.*?)'}", $link, $M)) {
+ $there = $M[1];
+ if ($here == $there) {
+ $class = " class='active'";
+ }
+ }
+ echo "<li$class>$link</li>\n";
+ }
+ }
+ echo "</ul>";
+ echo "</div>";
+
+}
+
function mtrack_mime_detect($filename, $namehint = null)
{
/* does config tell us how to decide mimetype */


diff -r 2be19816152164e9ea820d3687731c9953572bb0 -r 967ad07ad3a17edc2a042912c7f5688a76e1691c web/admin/auth.php
--- a/web/admin/auth.php
+++ b/web/admin/auth.php
@@ -135,6 +135,7 @@
}

mtrack_head("Administration - Authentication");
+mtrack_admin_nav();

$plugins = MTrackConfig::getSection('plugins');
$http_configd = isset($plugins['MTrackAuth_HTTP']) ? " (Active)" : '';


diff -r 2be19816152164e9ea820d3687731c9953572bb0 -r 967ad07ad3a17edc2a042912c7f5688a76e1691c web/admin/component.php
--- a/web/admin/component.php
+++ b/web/admin/component.php
@@ -57,6 +57,7 @@
}

mtrack_head("Administration - Components");
+mtrack_admin_nav();

echo "<form method='post'>";
echo "<br><b>Components</b><br>\n";


diff -r 2be19816152164e9ea820d3687731c9953572bb0 -r 967ad07ad3a17edc2a042912c7f5688a76e1691c web/admin/customfield.php
--- a/web/admin/customfield.php
+++ b/web/admin/customfield.php
@@ -46,6 +46,7 @@
}

mtrack_head("Administration - Custom Fields");
+mtrack_admin_nav();
echo "<h1>Custom Fields</h1>";




diff -r 2be19816152164e9ea820d3687731c9953572bb0 -r 967ad07ad3a17edc2a042912c7f5688a76e1691c web/admin/enum.php
--- a/web/admin/enum.php
+++ b/web/admin/enum.php
@@ -18,6 +18,7 @@
}

mtrack_head("Administration - $ename");
+mtrack_admin_nav();

$url = $enums[$ename];
$col = json_encode(MTrackAPI::invoke('GET', $url)->result);


diff -r 2be19816152164e9ea820d3687731c9953572bb0 -r 967ad07ad3a17edc2a042912c7f5688a76e1691c web/admin/index.php
--- a/web/admin/index.php
+++ b/web/admin/index.php
@@ -4,74 +4,8 @@

mtrack_head("Administration");

-$cat_titles = array(
- 'tickets' => 'Configure Tickets',
- 'projects' => 'Configure Projects &amp; Notifications',
- 'repo' => 'Configure Repositories',
- 'user' => 'User Administration &amp; Authentication',
- 'logs' => 'Review Logs',
-);
+mtrack_admin_nav();

-$by_cat = array();
-
-function add_cat($url) {
- global $by_cat;
- $cats = func_get_args();
- array_shift($cats);
- foreach ($cats as $cat) {
- $by_cat[$cat][] = $url;
- }
-}
-
-if (MTrackACL::hasAnyRights('Projects', 'modify')) {
- add_cat("<a href='{$ABSWEB}admin/project.php'>Projects and their notification settings</a>", 'projects');
-}
-
-if (MTrackACL::hasAnyRights('Enumerations', 'modify')) {
- $eurl = $ABSWEB . 'admin/enum.php';
- add_cat("<a href='$eurl/Priority'>Priority</a>", 'tickets');
- add_cat("<a href='$eurl/TicketState'>TicketState</a>", 'tickets');
- add_cat("<a href='$eurl/Severity'>Severity</a>", 'tickets');
- add_cat("<a href='$eurl/Resolution'>Resolution</a>", 'tickets');
- add_cat("<a href='$eurl/Classification'>Classification</a>", 'tickets');
- add_cat("<a href='{$ABSWEB}admin/customfield.php'>Custom Fields</a>", 'tickets');
-}
-
-if (MTrackACL::hasAnyRights('Components', 'modify')) {
- add_cat("<a href='{$ABSWEB}admin/component.php'>Components</a>", 'tickets', 'projects');
-}
-
-if (MTrackACL::hasAnyRights('Tickets', 'create')) {
- add_cat("<a href='{$ABSWEB}admin/importcsv.php'>Import Tickets from a CSV file</a>", 'tickets');
-}
-
-if (MTrackACL::hasAnyRights('Browser', 'modify')) {
- add_cat("<a href='{$ABSWEB}admin/repo.php'>Repositories and their links to Projects</a>", 'repo');
-}
-
-if (MTrackACL::hasAllRights('User', 'modify')) {
- add_cat("<a href='{$ABSWEB}admin/auth.php'>Authentication</a>", 'user');
- add_cat("<a href='{$ABSWEB}admin/user.php'>Users</a>", 'user');
-}
-
-if (MTrackACL::hasAllRights('Browser', 'modify')) {
- add_cat("<a href='{$ABSWEB}admin/logs.php'>Indexer logs</a>", 'logs');
-}
-
-echo "<div class='well'>";
-echo "<ul class='nav nav-list'>\n";
-foreach ($cat_titles as $cat => $title) {
- $links = $by_cat[$cat];
- if (count($links) == 0) {
- continue;
- }
- echo "<li class='nav-header'>$title</li>";
- foreach ($links as $link) {
- echo "<li>$link</li>\n";
- }
-}
-echo "</ul>";
-echo "</div>";

mtrack_foot();



diff -r 2be19816152164e9ea820d3687731c9953572bb0 -r 967ad07ad3a17edc2a042912c7f5688a76e1691c web/admin/logs.php
--- a/web/admin/logs.php
+++ b/web/admin/logs.php
@@ -13,6 +13,7 @@
}

mtrack_head("Logs");
+mtrack_admin_nav();

$vardir = MTrackConfig::get('core', 'vardir');
$filename = "$vardir/indexer.log";


diff -r 2be19816152164e9ea820d3687731c9953572bb0 -r 967ad07ad3a17edc2a042912c7f5688a76e1691c web/admin/project.php
--- a/web/admin/project.php
+++ b/web/admin/project.php
@@ -5,6 +5,7 @@
MTrackACL::requireAnyRights('Projects', 'modify');

mtrack_head("Administration - Projects");
+mtrack_admin_nav();

$projects = json_encode(MTrackAPI::invoke('GET', '/project/')->result);



diff -r 2be19816152164e9ea820d3687731c9953572bb0 -r 967ad07ad3a17edc2a042912c7f5688a76e1691c web/admin/user.php
--- a/web/admin/user.php
+++ b/web/admin/user.php
@@ -7,6 +7,7 @@
MTrackACL::requireAnyRights('User', 'modify');

mtrack_head("Administration - Users");
+mtrack_admin_nav();

?><h1>Users</h1>


diff -r 2be19816152164e9ea820d3687731c9953572bb0 -r 967ad07ad3a17edc2a042912c7f5688a76e1691c web/mtrack.css
--- a/web/mtrack.css
+++ b/web/mtrack.css
@@ -663,6 +663,13 @@
margin-left: 0px;
}

+#adminnav {
+ width: 20em;
+ float: left;
+ margin-right: 2em;
+ height: 100%;
+}
+
@media print {
/* print styling from html5boilerplate */
* { background: transparent !important; color: black !important; text-shadow: none !important; filter:none !important;



https://bitbucket.org/wez/mtrack/changeset/336f97c3569f/
changeset: 336f97c3569f
user: wez
date: 2012-04-22 02:01:00
summary: icon for auth
affected #: 1 file

diff -r 967ad07ad3a17edc2a042912c7f5688a76e1691c -r 336f97c3569fc496036949555c1f292373fe7922 inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -1166,7 +1166,7 @@
}

if (MTrackACL::hasAllRights('User', 'modify')) {
- $add_cat($by_cat, "<a href='{$ABSWEB}admin/auth.php'>Authentication</a>", 'user');
+ $add_cat($by_cat, "<a href='{$ABSWEB}admin/auth.php'><i class='icon-lock'></i> Authentication</a>", 'user');
$add_cat($by_cat, "<a href='{$ABSWEB}admin/user.php'><i class='icon-user'></i> Users</a>", 'user');
}




https://bitbucket.org/wez/mtrack/changeset/ea58525f3c34/
changeset: ea58525f3c34
user: wez
date: 2012-04-22 22:58:52
summary: backbone-ifying the repo admin screens.
affected #: 13 files

diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 inc/scm.php
--- a/inc/scm.php
+++ b/inc/scm.php
@@ -612,6 +612,7 @@
foreach ($this->links_to_remove as $linkid) {
MTrackDB::q('delete from project_repo_link where repoid = ? and linkid = ?', $this->repoid, $linkid);
}
+ $this->links = null;
}

function getLinks()
@@ -718,6 +719,224 @@
return $longest_id;
}

+ static function rest_return(MTrackRepo $r) {
+ $o = MTrackAPI::makeObj($r, 'repoid');
+
+ $o->checkout_command = $r->getCheckoutCommand();
+ $o->description_html = MTrackWiki::format_to_html($o->description);
+ $o->browsepath = $r->getBrowseRootName();
+
+ $o->links = array();
+ foreach ($r->getLinks() as $lid => $data) {
+ list($pid, $regex) = $data;
+ $l = new stdclass;
+ $l->id = $lid;
+ $l->project = $pid;
+ $l->regex = $regex;
+ $o->links[] = $l;
+ }
+
+ if ($r->canFork() && MTrackACL::hasAllRights('Browser', 'fork')
+ && MTrackConfig::get('repos', 'allow_user_repo_creation')) {
+ $o->canFork = true;
+ } else {
+ $o->canFork = false;
+ }
+ return $o;
+ }
+
+ static function rest_apply(MTrackChangeset $CS, MTrackRepo $r, $in) {
+
+ if (isset($in->description)) {
+ $r->description = $in->description;
+ }
+
+ /* parse and apply links.
+ * [{id: 1, regex: "path", project: 123}]
+ */
+ if (isset($in->links)) {
+ $current = $r->getLinks();
+ $seen = array();
+ foreach ($in->links as $link) {
+ /* updating an existing link? */
+ if (isset($link->id)) {
+ $seen[$link->id] = $link->id;
+ if (isset($current[$link->id])) {
+ /* already exists; are we changing it? */
+ list($pid, $regex) = $current[$link->id];
+ if ($pid != $link->project || $regex != $link->regex) {
+ $r->removeLink($link->id);
+ if (strlen($link->regex)) {
+ $r->addLink($link->project, $link->regex);
+ }
+ }
+ continue;
+ }
+ }
+ /* adding */
+ if (strlen($link->regex)) {
+ $r->addLink($link->project, $link->regex);
+ }
+ }
+ /* anything in current that is not in seen is removed */
+ foreach ($current as $lid => $data) {
+ if (isset($seen[$lid])) continue;
+ $r->removeLink($lid);
+ }
+ }
+
+ $r->save($CS);
+ $CS->setObject("repo:$r->repoid");
+
+ /* FIXME: perms
+ if (isset($_POST['perms'])) {
+ $perms = json_decode($_POST['perms']);
+ MTrackACL::setACL("repo:$P->repoid", 0, $perms);
+ }
+ */
+ }
+
+ /* /repo/properties -> lists or creates repos
+ */
+ static function rest_repo_list($method, $uri, $captures) {
+ MTrackAPI::checkAllowed($method, 'GET', 'POST');
+
+ if ($method == 'GET') {
+ MTrackACL::requireAllRights('Browser', 'read');
+
+ $res = array();
+ foreach (self::getReposList() as $r) {
+ if (!MTrackACL::hasAnyRights("repo:$repo->repoid", 'read')) {
+ continue;
+ }
+ $res[] = self::rest_return(self::loadById($r->repoid));
+ }
+ return $res;
+ }
+
+ MTrackACL::requireAnyRights('Browser', array('create', 'fork'));
+ $repo = new MTrackRepo;
+
+ /* we're creating a new guy here.
+ * We should validate that the current user has rights to
+ * use the specified parent ($owner) or access to the source
+ * repo (clonedfrom) */
+ /* FIXME: also respect allow_user_repo_creation */
+ /* If $owner != $me, then $owner can be any project that
+ * I have 'modify' rights for */
+
+ $in = MTrackAPI::getPayload();
+ if (!is_object($in)) {
+ MTrackAPI::error(400, "expected json payload");
+ }
+
+ if (!isset($in->shortname) || strlen(trim($in->shortname)) == 0) {
+ MTrackAPI::error(400, "invalid name", $in->shortname);
+ }
+ $in->shortname = trim($in->shortname);
+
+ if (preg_match("/[^a-zA-Z0-9_.-]/", $in->shortname)) {
+ MTrackAPI::error(400, "name contains illegal characters", $in->shortname);
+ }
+ if (MTrackACL::hasAnyRights('Browser', 'create')) {
+ /* I can create anything I damned well please */
+ } else {
+ /* I can only put things in my own namespace */
+ $me = mtrack_canon_username(MTrackAuth::whoami());
+ if ($in->parent != "user:$me") {
+ MTrackAPI::error(400, "owner must match my user",
+ $in->parent, "user:$me");
+ }
+ }
+ if (isset($in->clonedfrom)) {
+ MTrackACL::requireAllRights('Browser', 'fork');
+ MTrackACL::requireAllRights("repo:$in->clonedfrom", 'read');
+
+ $S = MTrackRepo::loadById($in->clonedfrom);
+ if (!$S->canFork()) {
+ MTrackAPI::error(400, "cannot fork repo", $S->shortname, $S->scmtype);
+ }
+ if (!isset($in->description)) {
+ $in->description = $S->description;
+ }
+ $repo->scmtype = $S->scmtype;
+ $repo->clonedfrom = $S->repoid;
+ } else {
+ if (!isset($in->scmtype)) {
+ MTrackAPI::error(400, "missing scmtype");
+ }
+ $repo->scmtype = $in->scmtype;
+ }
+ if (isset($in->parent)) {
+ $repo->parent = $in->parent;
+ }
+ $repo->shortname = $in->shortname;
+
+ $CS = MTrackChangeset::begin("repo:X", "Create repo $in->shortname");
+ self::rest_apply($CS, $repo, $in);
+ $CS->commit();
+ MTrackACL::requireAllRights("repo:$repo->repoid", 'read');
+ return self::rest_return($repo);
+ }
+
+ /* /repo/properties/123 -> details of repo
+ */
+ static function rest_props($method, $uri, $captures) {
+ MTrackAPI::checkAllowed($method, 'GET', 'PUT', 'DELETE');
+ MTrackACL::requireAllRights('Browser', 'read');
+
+ $rid = $captures['rid'];
+
+ $repo = self::loadById($rid);
+ if (!$repo) {
+ MTrackAPI::error(404, "invalid repo", $rid);
+ }
+ if ($method == 'DELETE') {
+ MTrackACL::requireAllRights("repo:$rid", 'delete');
+ $CS = MTrackChangeset::begin("repo:$rid", "Delete repo $repo->shortname");
+ $repo->deleteRepo($CS);
+ $CS->commit();
+ return;
+ }
+ if ($method != 'GET') {
+ $in = MTrackAPI::getPayload();
+ if (!is_object($in)) {
+ MTrackAPI::error(400, "expected json payload");
+ }
+ MTrackACL::requireAllRights("repo:$repo->repoid", 'modify');
+
+ $CS = MTrackChangeset::begin("repo:$repo->repoid",
+ "Edit repo $repo->shortname");
+ self::rest_apply($CS, $repo, $in);
+
+ $CS->commit();
+ }
+ MTrackACL::requireAllRights("repo:$repo->repoid", 'read');
+ return self::rest_return($repo);
+ }
+
+ /** returns a list of allowed owners for new repos for the
+ * authenticated user */
+ static function rest_allowed_targets($method, $uri, $captures) {
+ $res = array();
+
+ if (MTrackACL::hasAllRights('Browser', 'create')) {
+ $me = mtrack_canon_username(MTrackAuth::whoami());
+ $res = array("user:$me" => $me);
+
+ foreach (MTrackDB::q(
+ 'select projid, shortname, name from projects order by ordinal')
+ as $row)
+ {
+ if (MTrackACL::hasAllRights("project:$row[0]", 'modify')) {
+ $res['project:' . $row[1]] = $row[1];
+ }
+ }
+ }
+
+ return $res;
+ }
+
/* /repo/history/default/wiki
* GET params:
* rev: revision
@@ -884,9 +1103,13 @@

}

+MTrackAPI::register('/repo/properties', 'MTrackRepo::rest_repo_list');
+MTrackAPI::register('/repo/properties/:rid', 'MTrackRepo::rest_props');
MTrackAPI::register('/repo/history/*path', 'MTrackRepo::rest_history');
+MTrackAPI::register('/repo/allowed-targets', 'MTrackRepo::rest_allowed_targets');
MTrackLink::register('changeset', 'MTrackRepo::resolve_changeset_link');
MTrackLink::register('repo', 'MTrackRepo::resolve_repo_link');
MTrackLink::register('log', 'MTrackRepo::resolve_log_link');
MTrackLink::register('source', 'MTrackRepo::resolve_source_link');

+


diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -1132,11 +1132,11 @@
global $ABSWEB;

$cat_titles = array(
+ 'user' => 'User Administration &amp; Authentication',
+ 'repo' => 'Configure Repositories',
+ 'projects' => 'Projects &amp; Notifications',
'tickets' => 'Configure Tickets',
- 'projects' => 'Configure Projects &amp; Notifications',
- 'repo' => 'Configure Repositories',
- 'user' => 'User Administration &amp; Authentication',
- 'logs' => 'Review Logs',
+ 'logs' => 'Initial Setup &amp; Logs',
'import' => 'Import',
);

@@ -1150,7 +1150,7 @@
if (MTrackACL::hasAnyRights('Enumerations', 'modify')) {
$eurl = $ABSWEB . 'admin/enum.php';
$add_cat($by_cat, "<a href='$eurl/Priority'><i class='icon-list'></i> Priority</a>", 'tickets');
- $add_cat($by_cat, "<a href='$eurl/TicketState'><i class='icon-list'></i> TicketState</a>", 'tickets');
+ $add_cat($by_cat, "<a href='$eurl/TicketState'><i class='icon-list'></i> States</a>", 'tickets');
$add_cat($by_cat, "<a href='$eurl/Severity'><i class='icon-list'></i> Severity</a>", 'tickets');
$add_cat($by_cat, "<a href='$eurl/Resolution'><i class='icon-list'></i> Resolution</a>", 'tickets');
$add_cat($by_cat, "<a href='$eurl/Classification'><i class='icon-list'></i> Classification</a>", 'tickets');
@@ -1162,16 +1162,17 @@
}

if (MTrackACL::hasAnyRights('Browser', 'modify')) {
- $add_cat($by_cat, "<a href='{$ABSWEB}admin/repo.php'><i class='icon-file'></i> Repositories and their links to Projects</a>", 'repo');
+ $add_cat($by_cat, "<a href='{$ABSWEB}admin/repo.php'><i class='icon-file'></i> Repositories</a>", 'repo');
}

if (MTrackACL::hasAllRights('User', 'modify')) {
- $add_cat($by_cat, "<a href='{$ABSWEB}admin/auth.php'><i class='icon-lock'></i> Authentication</a>", 'user');
+ $add_cat($by_cat, "<a href='{$ABSWEB}admin/auth.php'><i class='icon-lock'></i> Authentication</a>", 'logs');
$add_cat($by_cat, "<a href='{$ABSWEB}admin/user.php'><i class='icon-user'></i> Users</a>", 'user');
+ $add_cat($by_cat, "<a href='{$ABSWEB}admin/group.php'><i class='icon-user'></i> Groups</a>", 'user');
}

if (MTrackACL::hasAnyRights('Tickets', 'create')) {
- $add_cat($by_cat, "<a class='btn' href='{$ABSWEB}admin/importcsv.php'><i class='icon-upload'></i> Import Tickets from a CSV file</a>", 'import');
+ $add_cat($by_cat, "<a class='btn' href='{$ABSWEB}admin/importcsv.php'><i class='icon-upload'></i> Import CSV</a>", 'import');
}

if (MTrackACL::hasAllRights('Browser', 'modify')) {


diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 web/admin/deleterepo.php
--- a/web/admin/deleterepo.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php # vim:ts=2:sw=2:et:
-/* For licensing and copyright terms, see the file named LICENSE */
-include '../../inc/common.php';
-
-if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- $rid = $_POST['repoid'];
-
- MTrackACL::requireAllRights("repo:$rid", 'delete');
-
- $S = MTrackRepo::loadById($rid);
- $CS = MTrackChangeset::begin("repo:$rid", "Delete repo $S->shortname");
- $S->deleteRepo($CS);
- $CS->commit();
-}
-
-header("Location: ${ABSWEB}browse.php");
-exit;
-


diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 web/admin/forkrepo.php
--- a/web/admin/forkrepo.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php # vim:ts=2:sw=2:et:
-/* For licensing and copyright terms, see the file named LICENSE */
-include '../../inc/common.php';
-
-MTrackACL::requireAnyRights('Browser', 'fork');
-
-if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- $rid = $_POST['source'];
- MTrackACL::requireAnyRights("repo:$rid", 'read');
- $name = trim($_POST['name']);
-
- if (strlen($name) == 0) {
- throw new Exception("missing name");
- }
- if (preg_match("/[^a-zA-Z0-9_.-]/", $name)) {
- throw new Exception("$name contains illegal characters");
- }
- $owner = mtrack_canon_username(MTrackAuth::whoami());
- if (preg_match("/[^a-zA-Z0-9_.-]/", $owner)) {
- throw new Exception("$owner must be a locally defined user");
- }
-
- $S = MTrackRepo::loadById($rid);
- if (!$S->canFork()) {
- throw new Exception("cannot fork this repo");
- }
- $P = new MTrackRepo;
- $P->shortname = $name;
- if (isset($_POST['repo:parent'])) {
- // FIXME: ACL check to see if we're allowed to create under the specified
- // parent
- $P->parent = $_POST['repo:parent'];
- } else {
- $P->parent = "user:$owner";
- }
-
- $P->scmtype = $S->scmtype;
- $P->description = $S->description;
- $P->clonedfrom = $S->repoid;
-
- $CS = MTrackChangeset::begin("repo:X",
- "Clone repo $S->shortname as $P->shortname");
- $P->save($CS);
- $CS->setObject("repo:$P->repoid");
- $CS->commit();
- $name = $P->getBrowseRootName();
- header("Location: ${ABSWEB}browse.php/$name");
- exit;
-}
-
-header("Location: ${ABSWEB}browse.php");
-exit;
-


diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 web/admin/group.php
--- a/web/admin/group.php
+++ b/web/admin/group.php
@@ -50,6 +50,7 @@
}

mtrack_head($group ? "$P->name - $group" : "$P->name - New Group");
+mtrack_admin_nav();

echo "<form method='post'><input type='hidden' name='pid' value='$pid'>";
if ($group) {


diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 web/admin/repo.php
--- a/web/admin/repo.php
+++ b/web/admin/repo.php
@@ -4,99 +4,8 @@

$rid = mtrack_get_pathinfo();

-if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- if ($rid == 'new') {
- MTrackACL::requireAnyRights('Browser', array('create', 'fork'));
- $P = new MTrackRepo;
- } else {
- MTrackACL::requireAnyRights("repo:$rid", 'modify');
- $P = MTrackRepo::loadById($rid);
- }
- $links = $P->getLinks();
- $plinks = array();
-
- foreach ($_POST as $name => $value) {
- if (preg_match("/^link:(\d+|new):project$/", $name, $M)) {
- $lid = $M[1];
- $plinks[$lid] = array(
- (int)$_POST["link:$lid:project"],
- trim($_POST["link:$lid:regex"]));
- }
- }
- if (isset($plinks['new'])) {
- $n = $plinks['new'];
- unset($plinks['new']);
- if (strlen($n[1])) {
- $P->addLink($n[0], $n[1]);
- }
- }
- foreach ($plinks as $lid => $n) {
- if (isset($links[$lid])) {
- if ($n != $links[$lid] || !strlen($n[1])) {
- $P->removeLink($lid);
- if (strlen($n[1])) {
- $P->addLink($n[0], $n[1]);
- }
- }
- } else if (strlen($n[1])) {
- $P->addLink($n[0], $n[1]);
- }
- }
-
- $restricted = !MTrackACL::hasAnyRights('Browser', 'create');
- if ($rid == 'new') {
- if (isset($_POST['repo:name'])) {
- $P->shortname = $_POST["repo:name"];
- }
- if (isset($_POST['repo:type'])) {
- $P->scmtype = $_POST["repo:type"];
- }
- if (isset($_POST['repo:path'])) {
- if ($restricted) throw new Exception("cannot set the repo path");
- $P->repopath = $_POST["repo:path"];
- }
- if (isset($_POST['repo:parent']) && strlen($_POST['repo:parent'])) {
- $P->parent = $_POST["repo:parent"];
- }
- } else {
- $editable = !strlen($P->parent);
-
- if (isset($_POST['repo:name']) && $_POST['repo:name'] != $P->shortname) {
- if (!$editable) throw new Exception("cannot change the repo name");
- $P->shortname = $_POST["repo:name"];
- }
- if (isset($_POST['repo:type']) && $_POST['repo:type'] != $P->scmtype) {
- if (!$editable) throw new Exception("cannot change the repo type");
- $P->scmtype = $_POST["repo:type"];
- }
- if (isset($_POST['repo:path']) && $_POST['repo:path'] != $P->repopath) {
- if (!$editable) throw new Exception("cannot change the repo path");
- $P->repopath = $_POST["repo:path"];
- }
- if (isset($_POST['repo:parent']) && $_POST['repo:parent'] != $P->parent) {
- if (!$editable) throw new Exception("cannot change the repo parent");
- $P->parent = $_POST["repo:parent"];
- }
- }
- if (isset($_POST["repo:description"])) {
- $P->description = $_POST["repo:description"];
- }
-
- $CS = MTrackChangeset::begin("repo:$rid", "Edit repo $P->shortname");
- $P->save($CS);
- $CS->setObject("repo:$P->repoid");
-
- if (isset($_POST['perms'])) {
- $perms = json_decode($_POST['perms']);
- MTrackACL::setACL("repo:$P->repoid", 0, $perms);
- }
-
- $CS->commit();
- header("Location: ${ABSWEB}browse.php/" . $P->getBrowseRootName());
- exit;
-}
-
mtrack_head("Administration - Repositories");
+mtrack_admin_nav();
if (!strlen($rid)) {
MTrackACL::requireAnyRights('Browser', 'modify');
?>
@@ -114,21 +23,72 @@
wiki pages. Click on the repository name to edit it, or click on the "Add"
button to tell mtrack to use another repository.
</p>
-<ul>
+<ul id='repolist' class='nav'></ul><?php
- foreach (MTrackDB::q(
- 'select repoid, shortname, parent from repos order by parent, shortname')
- as $row) {
- $rid = $row[0];
- if (MTrackACL::hasAnyRights("repo:$rid", 'modify')) {
- $name = MTrackSCM::makeDisplayName($row);
- $name = htmlentities($name, ENT_QUOTES, 'utf-8');
- echo "<li><a href='{$ABSWEB}admin/repo.php/$rid'>$name</a></li>\n";
- }
+ $repos = json_encode(MTrackAPI::invoke('GET', '/repo/properties')->result);
+ echo <<<HTML
+ <script>
+$(document).ready(function () {
+ var repos = new MTrackRepoList($repos);
+
+ function addOne(R) {
+ var ul = $('#repolist');
+ var li = $('<li/>');
+ var name = $('<a/>');
+ name.text(R.get('browsepath'));
+ name.attr('href', '#');//ABSWEB + 'admin/repo.php/' + R.id);
+ li.append(name);
+ li.data('model', R);
+ ul.append(li);
}
- echo "</ul>";
+
+ repos.bind('add', function (R) {
+ addOne(R);
+ });
+
+ function show_repo_dlg(R) {
+ console.log("show_repo_dlg");
+ var V = new MTrackRepoEditView({
+ model: R
+ });
+ V.show(function () {
+ repos.add(R, {at: repos.length});
+ console.log("done");
+ });
+ }
+
+ function draw_list() {
+ var ul = $('#repolist');
+ ul.empty();
+
+ repos.each(function (R) {
+ addOne(R);
+ });
+ }
+ draw_list();
+
+ $('ul#repolist').on('click', 'a', function() {
+ var R = $(this).closest('li').data('model');
+ var V = new MTrackRepoEditView({
+ model: R
+ });
+ V.show(function () {
+ console.log("done");
+ });
+
+ return false;
+ });
+
+ $('#newrepobtn').click(function () {
+ var R = new MTrackRepo;
+ show_repo_dlg(R);
+ });
+});
+</script>
+HTML;
+
if (MTrackACL::hasAnyRights('Browser', 'create')) {
- echo "<a href='{$ABSWEB}admin/repo.php/new'>Add new repo</a><br>\n";
+ echo "<button id='newrepobtn' class='btn btn-primary'><i class='icon-white icon-plus'></i> New Repo</button>";
}
mtrack_foot();
exit;


diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 web/browse.php
--- a/web/browse.php
+++ b/web/browse.php
@@ -202,13 +202,14 @@
as $row)
{
if (MTrackACL::hasAllRights("project:$row[0]", 'modify')) {
- $owners["project:$row[1]"] = $row[1];
+ $owners['project:' . $row[1]] = $row[1];
}
}
if (count($owners) > 1) {
- $owners = mtrack_select_box('repo:parent', $owners, null, true);
+ $owners = mtrack_select_box('parent', $owners, null, true);
} else {
- $owners = '';
+ $owners = "<input type='hidden' name='parent' value='" .
+ htmlentities($me, ENT_QUOTES, 'utf-8') . "'>";
}
}

@@ -244,9 +245,6 @@
<a class='close' data-dismiss='modal'>&times;</a><h3>Fork a repo</h3></div>
- <form id='forkform'
- action='${ABSWEB}admin/forkrepo.php' method='post'>
- <input type='hidden' name='source' value='$repo->repoid'><div class='modal-body'><p>
A fork is your own copy of a repo that is stored and maintained
@@ -272,13 +270,51 @@
</div><div class='modal-footer'><button class='btn' data-dismiss='modal'>Cancel</button>
- <button type='submit'
+ <button id='createforkbtn'
class='btn btn-primary'>Fork</button></div>
- </form></div><button id='forkbtn' class='btn' type='button'
data-toggle='modal' data-target='#forkdialog'><i class="icon-random"></i> Fork</button>
+<script>
+$(document).ready(function () {
+ function capture_error(model, resp) {
+ var err;
+ if (!_.isObject(resp)) {
+ err = resp;
+ } else {
+ err = resp.statusText;
+ try {
+ var r = JSON.parse(resp.responseText);
+ err = r.message;
+ } catch (e) {
+ }
+ }
+ $('<div class="alert alert-danger">' +
+ "<a class='close' data-dismiss='alert'>&times;</a>" +
+ err + '</div>').
+ appendTo('#forkdialog div.modal-body');
+ }
+
+ $('#createforkbtn').click(function () {
+ var dlg = $('#forkdialog');
+ var owner = $('select[name=parent]', dlg).val();
+ var name = $('input[name=name]', dlg).val();
+ var R = new MTrackRepo;
+ R.save({
+ parent: owner,
+ shortname: name,
+ clonedfrom: $repo->repoid,
+ }, {
+ success: function (model, resp) {
+ window.location = ABSWEB + "browse.php/" + model.get('browsepath');
+ },
+ error: capture_error
+ });
+ });
+});
+</script>
+
HTML
;
}
@@ -291,9 +327,6 @@
<a class='close' data-dismiss='modal'>&times;</a><h3>Really delete this Repo?</h3></div>
- <form id='deleteform' action='${ABSWEB}admin/deleterepo.php'
- method='post'>
- <input type='hidden' name='repoid' value='$repo->repoid'><div class='modal-body'><p>Are you sure you want to delete this repo?</p><p><b>You cannot undo this action; any data will be permanently
@@ -301,20 +334,67 @@
</div><div class='modal-footer'><button class='btn' data-dismiss='modal'>Cancel</button>
- <button type='submit'
+ <button type='submit' id='dodelete'
class='btn btn-danger'>Delete</button></div>
- </form></div><button id='deletebtn' class='btn' type='button'
data-toggle='modal' data-target='#deletedialog'><i class="icon-trash"></i> Delete</button>
+<script>
+$(document).ready(function () {
+ function capture_error(model, resp) {
+ var err;
+ if (!_.isObject(resp)) {
+ err = resp;
+ } else {
+ err = resp.statusText;
+ try {
+ var r = JSON.parse(resp.responseText);
+ err = r.message;
+ } catch (e) {
+ }
+ }
+ $('<div class="alert alert-danger">' +
+ "<a class='close' data-dismiss='alert'>&times;</a>" +
+ err + '</div>').
+ appendTo('#deletedialog div.modal-body');
+ }
+
+ $('#dodelete').click(function () {
+ var R = new MTrackRepo({id: $repo->repoid});
+ R.destroy({
+ success: function (model, resp) {
+ window.location = ABSWEB + "browse.php";
+ },
+ error: capture_error
+ });
+ });
+});
+</script>
+
HTML
;
}
if (MTrackACL::hasAllRights("repo:$repo->repoid", "modify")) {
- echo <<<EDIT
-<button class='btn' onclick='document.location.href = "{$ABSWEB}admin/repo.php/$repo->repoid"; return false;'><i class="icon-edit"></i> Edit</button>
-EDIT
+ $repojson = json_encode(MTrackAPI::invoke(
+ 'GET', "/repo/properties/$repo->repoid")->result);
+ echo <<<HTML
+<button id='editrepobtn' class='btn'><i class="icon-edit"></i> Edit</button>
+<script>
+$(document).ready(function () {
+ $('#editrepobtn').click(function () {
+ var R = new MTrackRepo($repojson);
+ var V = new MTrackRepoEditView({
+ model: R
+ });
+ V.show(function () {
+ window.location = ABSWEB + 'browse.php/' + R.get('browsepath');
+ });
+ });
+});
+</script>
+HTML
+
;
}
MTrackWatch::renderWatchUI('repo', $repo->repoid);
@@ -326,48 +406,23 @@

if (!$repo && MTrackACL::hasAllRights('Browser', 'fork')
&& MTrackConfig::get('repos', 'allow_user_repo_creation')) {
-$repotypes = array();
-foreach (MTrackRepo::getAvailableSCMs() as $t => $r) {
- $d = $r->getSCMMetaData();
- $repotypes[$t] = $d['name'];
-}
-$repotypes = mtrack_select_box("repo:type", $repotypes, null, true);
echo <<<HTML
-<div id='newdialog' class='modal hide fade'>
- <div class='modal-header'>
- <a class='close' data-dismiss='modal'>&times;</a>
- <h3>Create a new Repo</h3>
- </div>
- <form id='newrepoform' action='${ABSWEB}admin/repo.php/new'
- method='post'>
-
- <div class='modal-body'>
- <p>
- Choose a name for your repo:<br>
- $owners <input type='text' name='repo:name' value='myrepo'>
- </p>
- <p>
- Choose a repository type:<br>
- $repotypes
- </p>
- <p>
- Description:<br>
- <em>You may use <a href='{$ABSWEB}help.php/WikiFormatting'
- target='_blank'>WikiFormatting</a></em><br>
- <textarea name='repo:description' class='wiki shortwiki'
- rows='5' cols='78'></textarea>
- </div>
- <div class='modal-footer'>
- <button class='btn' data-dismiss='modal'>Cancel</button>
- <button type='submit'
- class='btn btn-primary'>Create Repo</button>
- </div>
- </form>
-</div><button id='newrepobtn' class='btn btn-primary'
- data-toggle='modal' data-target='#newdialog'
type='button'><i class='icon-plus icon-white'></i> New Repository</button><br>
+<script>
+$(document).ready(function () {
+ $('#newrepobtn').click(function () {
+ var R = new MTrackRepo;
+ var V = new MTrackRepoEditView({
+ model: R
+ });
+ V.show(function () {
+ window.location = ABSWEB + 'browse.php/' + R.get('browsepath');
+ });
+ });
+});
+</script>
HTML
;
}


diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 web/css/tw-bootstrap.css
--- a/web/css/tw-bootstrap.css
+++ b/web/css/tw-bootstrap.css
@@ -1797,6 +1797,108 @@
opacity: 1;
filter: alpha(opacity=100);
}
+
+.tabs-stacked .open > a:hover {
+ border-color: #999999;
+}
+.tabbable {
+ *zoom: 1;
+}
+.tabbable:before,
+.tabbable:after {
+ display: table;
+ content: "";
+}
+.tabbable:after {
+ clear: both;
+}
+.tab-content {
+ display: table;
+ width: 100%;
+}
+.tabs-below .nav-tabs,
+.tabs-right .nav-tabs,
+.tabs-left .nav-tabs {
+ border-bottom: 0;
+}
+.tab-content > .tab-pane,
+.pill-content > .pill-pane {
+ display: none;
+}
+.tab-content > .active,
+.pill-content > .active {
+ display: block;
+}
+.tabs-below .nav-tabs {
+ border-top: 1px solid #ddd;
+}
+.tabs-below .nav-tabs > li {
+ margin-top: -1px;
+ margin-bottom: 0;
+}
+.tabs-below .nav-tabs > li > a {
+ -webkit-border-radius: 0 0 4px 4px;
+ -moz-border-radius: 0 0 4px 4px;
+ border-radius: 0 0 4px 4px;
+}
+.tabs-below .nav-tabs > li > a:hover {
+ border-bottom-color: transparent;
+ border-top-color: #ddd;
+}
+.tabs-below .nav-tabs .active > a,
+.tabs-below .nav-tabs .active > a:hover {
+ border-color: transparent #ddd #ddd #ddd;
+}
+.tabs-left .nav-tabs > li,
+.tabs-right .nav-tabs > li {
+ float: none;
+}
+.tabs-left .nav-tabs > li > a,
+.tabs-right .nav-tabs > li > a {
+ min-width: 74px;
+ margin-right: 0;
+ margin-bottom: 3px;
+}
+.tabs-left .nav-tabs {
+ float: left;
+ margin-right: 19px;
+ border-right: 1px solid #ddd;
+}
+.tabs-left .nav-tabs > li > a {
+ margin-right: -1px;
+ -webkit-border-radius: 4px 0 0 4px;
+ -moz-border-radius: 4px 0 0 4px;
+ border-radius: 4px 0 0 4px;
+}
+.tabs-left .nav-tabs > li > a:hover {
+ border-color: #eeeeee #dddddd #eeeeee #eeeeee;
+}
+.tabs-left .nav-tabs .active > a,
+.tabs-left .nav-tabs .active > a:hover {
+ border-color: #ddd transparent #ddd #ddd;
+ *border-right-color: #ffffff;
+}
+.tabs-right .nav-tabs {
+ float: right;
+ margin-left: 19px;
+ border-left: 1px solid #ddd;
+}
+.tabs-right .nav-tabs > li > a {
+ margin-left: -1px;
+ -webkit-border-radius: 0 4px 4px 0;
+ -moz-border-radius: 0 4px 4px 0;
+ border-radius: 0 4px 4px 0;
+}
+.tabs-right .nav-tabs > li > a:hover {
+ border-color: #eeeeee #eeeeee #eeeeee #dddddd;
+}
+.tabs-right .nav-tabs .active > a,
+.tabs-right .nav-tabs .active > a:hover {
+ border-color: #ddd #ddd #ddd transparent;
+ *border-left-color: #ffffff;
+}
+
+
.navbar {
*position: relative;
*z-index: 2;


diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 web/js.php
--- a/web/js.php
+++ b/web/js.php
@@ -111,6 +111,28 @@
echo "var mtrack_wiki_templates = " . json_encode($templates) . ";\n";
echo "var mtrack_wiki_template_cache = {};\n";

+/* Available SCMs */
+$repotypes = array();
+foreach (MTrackRepo::getAvailableSCMs() as $t => $r) {
+ $d = $r->getSCMMetaData();
+ $repotypes[$t] = $d['name'];
+}
+echo "var mtrack_repotypes = " . json_encode($repotypes) . ";\n";
+echo "var mtrack_repotypes_select = " .
+ json_encode(mtrack_select_box('type', $repotypes, null, true)) . ";\n";
+
+/* "Compile" underscore templates into a blob that we always send down
+ * with this javascript blob */
+$templdir = dirname(__FILE__) . '/js/templates';
+$templates = array();
+foreach (scandir($templdir) as $name) {
+ if ($name[0] == '.') continue;
+ $id = preg_replace('/\.html$/', '', $name);
+ $id = preg_replace('/[^a-z0-9]+/', '-', $id);
+ $templates[$id] = file_get_contents("$templdir/$name");
+}
+echo "var mtrack_underscore_templates = " . json_encode($templates) . ";\n";
+
foreach ($scripts as $name) {
echo "\n// $name\n";
readfile("js/$name");


diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 web/js/models.js
--- a/web/js/models.js
+++ b/web/js/models.js
@@ -41,6 +41,28 @@
}
});

+var MTrackRepo = Backbone.Model.extend({
+ defaults: {
+ "shortname": '',
+ "description": '',
+ "links": []
+ },
+
+ url: function() {
+ if (this.isNew()) {
+ return ABSWEB + "api.php/repo/properties/";
+ }
+ return ABSWEB + "api.php/repo/properties/" + this.id;
+ }
+});
+
+var MTrackRepoList = Backbone.Collection.extend({
+ model: MTrackRepo,
+ url: function() {
+ return ABSWEB + "api.php/repo/properties";
+ }
+});
+
var MTrackTicketChange = Backbone.Model.extend({
});



diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 web/js/mtrack.js
--- a/web/js/mtrack.js
+++ b/web/js/mtrack.js
@@ -328,6 +328,36 @@
outlinetarget.html(menu);
}

+/* decodes an ajax error response and returns the reason string */
+function mtrack_ajax_error_string(resp) {
+ var err;
+ if (!_.isObject(resp)) {
+ return resp;
+ }
+ err = resp.statusText;
+ try {
+ var r = JSON.parse(resp.responseText);
+ err = r.message;
+ } catch (e) {
+ }
+ return err;
+}
+
+/* converts an ajax error to an alert and puts it into the DOM */
+function mtrack_ajax_error_to_dom(resp, container, cls)
+{
+ if (typeof(cls) == 'undefined') {
+ cls = 'alert-danger';
+ }
+
+ resp = mtrack_ajax_error_string(resp);
+
+ $('<div class="alert ' + cls + '">' +
+ "<a class='close' data-dismiss='alert'>&times;</a>" +
+ resp + '</div>').
+ appendTo(container);
+}
+
$(document).ready(function() {
jQuery.timeago.settings.allowFuture = true;
$('abbr.timeinterval').timeago();


diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -2127,3 +2127,159 @@
return this;
}
});
+
+MTrackRepoEditView = Backbone.View.extend({
+ initialize: function(options) {
+ this.options = options;
+ this.template = _.template(mtrack_underscore_templates['repo-edit']);
+// this.link_template = _.template(
+// mtrack_underscore_templates['repo-edit-link']);
+ },
+ show: function(on_success) {
+ var o = this.model.toJSON();
+ o.owner = o.parent;
+ delete o.parent;
+ o.isnew = this.model.isNew();
+ o.repotypes = mtrack_repotypes_select;
+ o.repotypenames = mtrack_repotypes;
+
+ $(this.el).html(this.template(o));
+
+ var view = this;
+ $(view.el).appendTo('body');
+ var dlg = $('div.modal', view.el);
+ dlg.on('hidden', function () {
+ $(view.el).remove();
+ });
+
+ /* get the list of eligible projects for notifications;
+ * when it arrives, update the notifications tab */
+ view.projects = new MTrackProjectCollection;
+
+ function fill_projects() {
+ $('select[name=project]', dlg).each(function () {
+ var sel = $(this);
+ view.projects.each(function (P) {
+ var opt = $('<option/>');
+ opt.attr('value', P.id);
+ var label = P.get('name');
+ if (P.get('notifyemail')) {
+ label += ' <' + P.get('notifyemail') + '>';
+ }
+ opt.text(label);
+ sel.append(opt);
+ });
+ // pre-select the correct project
+ sel.val(sel.attr('data-projid'));
+ });
+
+ /* on the notifications tab, update the text that says where
+ * the notifications will go */
+ var oproj = view.model.get('parent');
+ if (oproj) {
+ var m = oproj.match(/^project:(.*)$/);
+ if (m) {
+ /* look for the project name in our collection */
+ var P = view.projects.find(function (P) {
+ return P.get('shortname') == m[1];
+ });
+ $('b#ownername', dlg).text(P.get('name'));
+ var email = P.get('notifyemail');
+ if (!email) {
+ email = "nowhere";
+ }
+ $('b#owneremail', dlg).text(email);
+ }
+ }
+ }
+
+ $('div.newlink button', dlg).click(function () {
+ var regex = $('div.newlink input[name=regex]');
+ var proj = $('div.newlink select[name=project]');
+
+ if (!regex.val().match(/\S/)) {
+ return;
+ }
+
+ /* copy this up to the links section */
+ var link = $('div.newlink').clone();
+ link.removeClass('newlink');
+ $('button', link).remove();
+ $('div.links', dlg).append(link);
+ $('select[name=project]', link).val(proj.val());
+ regex.val('');
+ });
+
+ view.projects.fetch({
+ success: function() {
+ fill_projects();
+ },
+ error: function (col, resp) {
+ mtrack_ajax_error_to_dom(resp, $('div.modal-body', dlg));
+ }
+ });
+
+ /* populate list of allowed parents */
+ if (view.model.isNew()) {
+ $.ajax({
+ url: ABSWEB + "api.php/repo/allowed-targets",
+ success: function (data) {
+ var sel = $('select[name=parent]', dlg);
+ _.each(data, function (value, key) {
+ var opt = $('<option/>');
+ opt.attr('value', key);
+ opt.text(value);
+ sel.append(opt);
+ });
+ },
+ });
+ }
+
+ /* save button */
+ $('button.btn-primary', dlg).click(function () {
+ /* get fields out */
+ var owner = $('select[name=parent]', dlg).val();
+ var name = $('input[name=name]', dlg).val();
+ var desc = $('textarea[name=description]', dlg).val();
+ var typ = $('select[name=type]', dlg).val();
+
+ var links = [];
+ $('div.links div', dlg).each(function () {
+ var link = {};
+ var L = $(this);
+ if (L.attr('data-linkid')) {
+ link.id = L.attr('data-linkid');
+ }
+ link.regex = $('input[name=regex]', L).val();
+ link.project = $('select[name=project]', L).val();
+ links.push(link);
+ });
+
+ var data = {
+ links: links,
+ description: desc
+ };
+
+ if (view.model.isNew()) {
+ data.parent = owner;
+ data.shortname = name;
+ data.scmtype = typ;
+ }
+
+ view.model.save(data, {
+ success: function (model, resp) {
+ /* do something useful */
+ dlg.modal('hide');
+ on_success(model);
+ },
+ error: function (model, resp) {
+ mtrack_ajax_error_to_dom(resp, $('div.modal-body', dlg));
+ }
+ });
+ });
+
+ dlg.modal('show');
+ }
+
+});
+


diff -r 336f97c3569fc496036949555c1f292373fe7922 -r ea58525f3c34b543184205c1ef04cc277ff64569 web/mtrack.css
--- a/web/mtrack.css
+++ b/web/mtrack.css
@@ -664,7 +664,7 @@
}

#adminnav {
- width: 20em;
+ width: 15em;
float: left;
margin-right: 2em;
height: 100%;



https://bitbucket.org/wez/mtrack/changeset/e53051067c42/
changeset: e53051067c42
user: wez
date: 2012-04-22 23:00:15
summary: meant to add this file with the last commit
affected #: 1 file

diff -r ea58525f3c34b543184205c1ef04cc277ff64569 -r e53051067c4217927d2d724d80c9fb709ec73f76 web/js/templates/repo.edit.html
--- /dev/null
+++ b/web/js/templates/repo.edit.html
@@ -0,0 +1,91 @@
+<div class='modal hide'>
+ <div class='modal-header'>
+ <a class='close' data-dismiss='modal'>&times;</a>
+ <% if (isnew) { %>
+ <h3>Create a new Repo</h3>
+ <% } else { %>
+ <h3>Edit Repo <%- browsepath %></h3>
+ <% } %>
+ </div>
+ <div class='modal-body'>
+ <ul class="nav nav-tabs">
+ <li class='active'><a href='#repo-main' data-toggle='tab'>Details</a></li>
+ <li><a href='#repo-links' data-toggle='tab'>Notifications</a></li>
+ <li><a href='#repo-perms' data-toggle='tab'>Permissions</a></li>
+ </ul>
+ <div class='tab-content'>
+ <div class='tab-pane active' id='repo-main'>
+ <% if (isnew) { %>
+ <p>
+ Choose a name for your repo:<br>
+ <select name='parent'></select><input type='text' name='name'
+ value='<%- shortname %>' placeholder="Choose a shortname">
+ </p>
+ <p>
+ Choose a repository type:<br>
+ <%= repotypes %>
+ </p>
+ <% } else { %>
+ <p>This is a <%- repotypenames[scmtype] %> repository</p>
+ <% } %>
+ <br>
+ <p>
+ Description:<br>
+ <textarea name='description' class='wiki shortwiki'
+ placeholder='Enter a description; you may use WikiFormatting!'
+ rows='5' cols='78'
+ ><% if (description) { %><%- description %><% } %></textarea>
+ </p>
+ </div>
+
+ <div class='tab-pane' id='repo-links'>
+ <% if (owner) { %>
+ <p>
+ This repo is linked to <b id='ownername'><%- owner %></b>.
+ By default, email notifications will be sent to
+ <b id='owneremail'>them</b>.
+ </p>
+ <% } %>
+ <p>
+ You may configure links to other projects below; the longest
+ match will be taken and notification will be sent to the
+ associated project.
+ </p>
+ <br>
+<span class='alert alert-info'>
+The regex should just be the bare regex string--you must not enclose it in
+regex delimiters.
+</span>
+ <br>
+ <br>
+
+ <div class='links'>
+ <% _.each(links, function (link) { %>
+ <div data-linkid="<%- link.id %>">
+ <input name="regex" value="<%- link.regex %>"
+ placeholder="Enter path regex">
+ <select data-projid="<%- link.project %>" name="project"></select>
+ </div>
+ <% }); %>
+ </div>
+ <br>
+
+ <div class="newlink">
+ <input name="regex" placeholder="Define new path regex">
+ <select name="project"></select>
+ <button class='btn'><i class='icon-plus'></i> Add Link</button>
+ </div>
+
+ </div>
+
+ <div class='tab-pane' id='repo-perms'>
+ perms
+ </div>
+ </div>
+ </div>
+ <div class='modal-footer'>
+ <button class='btn' data-dismiss='modal'>Cancel</button>
+ <button class='btn btn-primary'
+ ><% if (isnew) { %>Create<% } else { %>Save<% } %> Repo</button>
+ </div>
+</div>



https://bitbucket.org/wez/mtrack/changeset/d94661383727/
changeset: d94661383727
user: wez
date: 2012-04-23 00:17:28
summary: add delete button to repo editor, remove it from browse area.
affected #: 7 files

diff -r e53051067c4217927d2d724d80c9fb709ec73f76 -r d94661383727157d524e4a1dc6379685e2e88eda inc/scm.php
--- a/inc/scm.php
+++ b/inc/scm.php
@@ -742,6 +742,14 @@
} else {
$o->canFork = false;
}
+
+ if ($r->parent &&
+ MTrackACL::hasAllRights("repo:$r->repoid", "delete")) {
+ $o->canDelete = true;
+ } else {
+ $o->canDelete = false;
+ }
+
return $o;
}



diff -r e53051067c4217927d2d724d80c9fb709ec73f76 -r d94661383727157d524e4a1dc6379685e2e88eda web/admin/repo.php
--- a/web/admin/repo.php
+++ b/web/admin/repo.php
@@ -45,15 +45,16 @@
repos.bind('add', function (R) {
addOne(R);
});
+ repos.bind('remove', function (R) {
+ draw_list();
+ });

function show_repo_dlg(R) {
- console.log("show_repo_dlg");
var V = new MTrackRepoEditView({
model: R
});
V.show(function () {
repos.add(R, {at: repos.length});
- console.log("done");
});
}

@@ -72,8 +73,7 @@
var V = new MTrackRepoEditView({
model: R
});
- V.show(function () {
- console.log("done");
+ V.show(function (model) {
});

return false;


diff -r e53051067c4217927d2d724d80c9fb709ec73f76 -r d94661383727157d524e4a1dc6379685e2e88eda web/browse.php
--- a/web/browse.php
+++ b/web/browse.php
@@ -319,62 +319,6 @@
;
}
$mine = "user:$me";
- if ($repo->parent &&
- MTrackACL::hasAllRights("repo:$repo->repoid", "delete")) {
- echo <<<HTML
-<div id='deletedialog' class='modal hide fade'>
- <div class='modal-header'>
- <a class='close' data-dismiss='modal'>&times;</a>
- <h3>Really delete this Repo?</h3>
- </div>
- <div class='modal-body'>
- <p>Are you sure you want to delete this repo?</p>
- <p><b>You cannot undo this action; any data will be permanently
- deleted</b></p>
- </div>
- <div class='modal-footer'>
- <button class='btn' data-dismiss='modal'>Cancel</button>
- <button type='submit' id='dodelete'
- class='btn btn-danger'>Delete</button>
- </div>
-</div>
-<button id='deletebtn' class='btn' type='button'
- data-toggle='modal' data-target='#deletedialog'><i class="icon-trash"></i> Delete</button>
-<script>
-$(document).ready(function () {
- function capture_error(model, resp) {
- var err;
- if (!_.isObject(resp)) {
- err = resp;
- } else {
- err = resp.statusText;
- try {
- var r = JSON.parse(resp.responseText);
- err = r.message;
- } catch (e) {
- }
- }
- $('<div class="alert alert-danger">' +
- "<a class='close' data-dismiss='alert'>&times;</a>" +
- err + '</div>').
- appendTo('#deletedialog div.modal-body');
- }
-
- $('#dodelete').click(function () {
- var R = new MTrackRepo({id: $repo->repoid});
- R.destroy({
- success: function (model, resp) {
- window.location = ABSWEB + "browse.php";
- },
- error: capture_error
- });
- });
-});
-</script>
-
-HTML
-;
- }
if (MTrackACL::hasAllRights("repo:$repo->repoid", "modify")) {
$repojson = json_encode(MTrackAPI::invoke(
'GET', "/repo/properties/$repo->repoid")->result);
@@ -387,8 +331,13 @@
var V = new MTrackRepoEditView({
model: R
});
- V.show(function () {
- window.location = ABSWEB + 'browse.php/' + R.get('browsepath');
+ V.show(function (model) {
+ if (model) {
+ window.location = ABSWEB + 'browse.php/' + model.get('browsepath');
+ } else {
+ // deleted
+ window.location = ABSWEB + 'browse.php';
+ }
});
});
});


diff -r e53051067c4217927d2d724d80c9fb709ec73f76 -r d94661383727157d524e4a1dc6379685e2e88eda web/js/models.js
--- a/web/js/models.js
+++ b/web/js/models.js
@@ -45,6 +45,7 @@
defaults: {
"shortname": '',
"description": '',
+ "canDelete": false,
"links": []
},



diff -r e53051067c4217927d2d724d80c9fb709ec73f76 -r d94661383727157d524e4a1dc6379685e2e88eda web/js/templates/repo.edit.html
--- a/web/js/templates/repo.edit.html
+++ b/web/js/templates/repo.edit.html
@@ -1,4 +1,4 @@
-<div class='modal hide'>
+<div class='modal hide repoeditor'><div class='modal-header'><a class='close' data-dismiss='modal'>&times;</a><% if (isnew) { %>
@@ -12,6 +12,10 @@
<li class='active'><a href='#repo-main' data-toggle='tab'>Details</a></li><li><a href='#repo-links' data-toggle='tab'>Notifications</a></li><li><a href='#repo-perms' data-toggle='tab'>Permissions</a></li>
+ <% if (canDelete) { %>
+ <li style='float:right'><a href='#repo-delete'
+ data-toggle='tab'>Delete</a></li>
+ <% } %></ul><div class='tab-content'><div class='tab-pane active' id='repo-main'>
@@ -62,7 +66,7 @@
<div class='links'><% _.each(links, function (link) { %><div data-linkid="<%- link.id %>">
- <input name="regex" value="<%- link.regex %>"
+ <input name="regex" type='text' value="<%- link.regex %>"
placeholder="Enter path regex"><select data-projid="<%- link.project %>" name="project"></select></div>
@@ -71,7 +75,7 @@
<br><div class="newlink">
- <input name="regex" placeholder="Define new path regex">
+ <input type='text' name="regex" placeholder="Define new path regex"><select name="project"></select><button class='btn'><i class='icon-plus'></i> Add Link</button></div>
@@ -81,6 +85,28 @@
<div class='tab-pane' id='repo-perms'>
perms
</div>
+
+ <% if (canDelete) { %>
+ <div class='tab-pane' id='repo-delete'>
+ <div class='alert alert-danger'>
+ <p>Are you sure you want to delete this repo?</p>
+ <p>
+ <b>You cannot undo this action; any data will be permanently
+ deleted!</b>
+ </p>
+ </div>
+ <p>
+ To confirm that you want to delete to repo, type in the repo
+ name below, then press the delete button.
+ </p>
+ <input type='text' name='deleteme' placeholder='Enter <%- shortname %> here to confirm'>
+ <br>
+ <button id='deletebtn' class='btn btn-danger'><i class='icon-white
+ icon-trash'></i> Confirm Delete</button>
+
+ </div>
+ <% } %>
+
</div></div><div class='modal-footer'>


diff -r e53051067c4217927d2d724d80c9fb709ec73f76 -r d94661383727157d524e4a1dc6379685e2e88eda web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -2235,6 +2235,21 @@
});
}

+ /* delete button */
+ $('#deletebtn', dlg).click(function () {
+ if ($('input[name=deleteme]', dlg).val() == view.model.get('shortname')) {
+ view.model.destroy({
+ success: function (model, resp) {
+ dlg.modal('hide');
+ on_success(null);
+ },
+ error: function (model, resp) {
+ mtrack_ajax_error_to_dom(resp, $('div.modal-body', dlg));
+ }
+ });
+ }
+ });
+
/* save button */
$('button.btn-primary', dlg).click(function () {
/* get fields out */


diff -r e53051067c4217927d2d724d80c9fb709ec73f76 -r d94661383727157d524e4a1dc6379685e2e88eda web/mtrack.css
--- a/web/mtrack.css
+++ b/web/mtrack.css
@@ -670,6 +670,15 @@
height: 100%;
}

+div.repoeditor input[type=text] {
+ width: 25em;
+}
+
+div.repoeditor p {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
@media print {
/* print styling from html5boilerplate */
* { background: transparent !important; color: black !important; text-shadow: none !important; filter:none !important;



https://bitbucket.org/wez/mtrack/changeset/7231ee6052f7/
changeset: 7231ee6052f7
user: wez
date: 2012-04-23 04:09:34
summary: backbone and ajaxy permissions editing for repos
affected #: 5 files

diff -r d94661383727157d524e4a1dc6379685e2e88eda -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 inc/acl.php
--- a/inc/acl.php
+++ b/inc/acl.php
@@ -283,6 +283,58 @@
}
}

+ /* computes the ACL object suitable for including in the JSON
+ * object returned (and settable) via the REST API.
+ * This is used by the various views to create ACL editors.
+ */
+ static public function computeACLObject($objectid) {
+ $o = new stdclass;
+ $o->acl = array();
+ $o->inherited = array();
+
+ foreach (self::getACL($objectid, 0) as $ent) {
+ $o->acl[] = array($ent['role'], $ent['action'], (int)$ent['allow']);
+ }
+
+ $path = self::getParentPath($objectid, 2);
+ if (count($path) == 2) {
+ foreach (self::getACL($path[1], 1) as $ent) {
+ $o->inherited[] = array($ent['role'], $ent['action'], (int)$ent['allow']);
+ }
+ }
+
+ return $o;
+ }
+
+ static function rest_roles($method, $uri, $captures) {
+ MTrackAPI::checkAllowed($method, 'GET');
+
+ /* avoid leaking information to anonymous users */
+ $me = MTrackAuth::whoami();
+ if ($me == 'anonymous' || MTrackAuth::getUserClass() == 'anonymous') {
+ $o = new stdclass;
+ return $o;
+ }
+
+ $groups = MTrackAuth::enumGroups();
+ /* merge in users */
+ foreach (MTrackDB::q(
+ 'select userid, fullname from userinfo where active = 1')
+ ->fetchAll() as $row) {
+ if (isset($groups[$row[0]])) continue;
+ if (strlen($row[1])) {
+ $disp = "$row[0] - $row[1]";
+ } else {
+ $disp = $row[0];
+ }
+ $groups[$row[0]] = $disp;
+ }
+ if (!isset($groups['*'])) {
+ $groups['*'] = '(Everybody)';
+ }
+ return $groups;
+ }
+
/* helper for generating an ACL editor.
* As parameters, takes an objectid indicating the object being edited,
* and an action map which breaks down tasks into groups.
@@ -594,3 +646,5 @@
}
}

+MTrackAPI::register('/acl/roles', 'MTrackACL::rest_roles');
+


diff -r d94661383727157d524e4a1dc6379685e2e88eda -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 inc/scm.php
--- a/inc/scm.php
+++ b/inc/scm.php
@@ -736,6 +736,10 @@
$o->links[] = $l;
}

+ if (MTrackACL::hasAllRights("repo:$r->repoid", "modify")) {
+ $o->perms = MTrackACL::computeACLObject("repo:$r->repoid");
+ }
+
if ($r->canFork() && MTrackACL::hasAllRights('Browser', 'fork')
&& MTrackConfig::get('repos', 'allow_user_repo_creation')) {
$o->canFork = true;
@@ -796,12 +800,9 @@
$r->save($CS);
$CS->setObject("repo:$r->repoid");

- /* FIXME: perms
- if (isset($_POST['perms'])) {
- $perms = json_decode($_POST['perms']);
- MTrackACL::setACL("repo:$P->repoid", 0, $perms);
- }
- */
+ if (isset($in->perms) && isset($in->perms->acl)) {
+ MTrackACL::setACL("repo:$r->repoid", 0, $in->perms->acl);
+ }
}

/* /repo/properties -> lists or creates repos


diff -r d94661383727157d524e4a1dc6379685e2e88eda -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 web/js/templates/acl.edit.html
--- /dev/null
+++ b/web/js/templates/acl.edit.html
@@ -0,0 +1,27 @@
+<div class='permissioneditor'>
+<p>
+ <b>Permissions</b>
+</p>
+<p>
+ <em>Select "Add" to define permissions for an entity.
+ The first matching permission is taken as definitive,
+ so if a given user belongs to multiple groups and matches
+ multiple permission rows, the first is taken. You may
+ drag to re-order permissions.
+ </em>
+</p>
+<p>
+ <em>Permissions inherited from the parent of this object are
+ shown as non-editable entries at the top of the list. You may
+ override them by adding your own explicit entry.</em>
+</p>
+<br>
+<table>
+ <thead>
+ <tr>
+ <th>Entity</th>
+ </tr>
+ </thead>
+ <tbody></tbody>
+</table>
+


diff -r d94661383727157d524e4a1dc6379685e2e88eda -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 web/js/templates/repo.edit.html
--- a/web/js/templates/repo.edit.html
+++ b/web/js/templates/repo.edit.html
@@ -11,7 +11,9 @@
<ul class="nav nav-tabs"><li class='active'><a href='#repo-main' data-toggle='tab'>Details</a></li><li><a href='#repo-links' data-toggle='tab'>Notifications</a></li>
+ <% if (!isnew) { %><li><a href='#repo-perms' data-toggle='tab'>Permissions</a></li>
+ <% } %><% if (canDelete) { %><li style='float:right'><a href='#repo-delete'
data-toggle='tab'>Delete</a></li>
@@ -82,9 +84,10 @@

</div>

+ <% if (!isnew) { %><div class='tab-pane' id='repo-perms'>
- perms
</div>
+ <% } %><% if (canDelete) { %><div class='tab-pane' id='repo-delete'>


diff -r d94661383727157d524e4a1dc6379685e2e88eda -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -2128,6 +2128,315 @@
}
});

+/* uses the "perms" attribute of the supplied model to render
+ * an ACL editor.
+ * Call the compute method to compile the new ACL ready for
+ * submission to the API endpoint when you want to save the model.
+ */
+MTrackACLEditView = Backbone.View.extend({
+ initialize: function (options) {
+ this.options = options;
+ this.action_map = options.action_map;
+ this.template = _.template(mtrack_underscore_templates['acl-edit']);
+ },
+ render: function() {
+ $(this.el).html(this.template({}));
+
+ var perms = this.model.get('perms');
+
+ /* first, compute a table with columns:
+ * Entity | Cat 1 | Cat 2
+ * Where Cat 1 through Cat n are the top level keys of the supplied
+ * action_map */
+
+ var tbody = $('tbody', $(this.el));
+
+ var tr = $('thead tr', $(this.el));
+ // Add columns for action groups
+ var cat_order = _.keys(this.action_map).sort(
+ function (a, b) {
+ return b - a;
+ }
+ );
+
+ /* a map for reverse engineering a permission to the appropriate
+ * group in the action map */
+ var reng = {};
+ var rank = {};
+ /* filled in with action_map and populated with the actual
+ * permissions values for each level in the map.
+ * eg: {SSH: ['-checkout|-commit', 'None']}
+ */
+ var mobj = {};
+ var groups = {};
+
+ for (var gi in cat_order) {
+ var group = cat_order[gi];
+ tr.append($('<th/>').text(group));
+
+ /* let's also build up a map to help us with later work */
+ var all_perms = _.keys(this.action_map[group]);
+ var prohibit = {};
+ _.each(this.action_map[group], function (caption, perm) {
+ prohibit[perm] = "-" + perm;
+ });
+ var none = _.values(prohibit).join('|');
+ var a = [[none, 'None']];
+ var accum = [];
+ var i = 0;
+ _.each(this.action_map[group], function (caption, perm) {
+ accum.push(perm);
+ delete prohibit[perm];
+ var p = _.clone(accum);
+ _.each(prohibit, function (E) {
+ p.push(E);
+ });
+ a.push([p.join('|'), caption]);
+ reng[perm] = group;
+ if (!(group in rank)) {
+ rank[group] = {};
+ }
+ rank[group][perm] = i++;
+ });
+ mobj[group] = a;
+ }
+
+ /* A helper function that processes an ACL like this:
+ * [['wez', 'checkout', 1], ['wez', 'commit', 1]]
+ * and turns it into this:
+ * {wez: {"SSH":['checkout', 'commit']}}
+ */
+ function group_actions(acl) {
+ var defs = {};
+ _.each(acl, function (ent) {
+ var role = ent[0];
+ var action = ent[1];
+ var allow = ent[2];
+
+ if (!(action in reng)) {
+ return;
+ }
+ var group = reng[action];
+ if (!allow) {
+ action = '-' + action;
+ }
+ if (!(role in defs)) {
+ defs[role] = {};
+ }
+ if (!(group in defs[role])) {
+ defs[role][group] = [];
+ }
+ defs[role][group].push(action);
+
+ if (!(role in groups)) {
+ groups[role] = role;
+ }
+ });
+ return defs;
+ }
+ var roledefs = group_actions(perms.acl);
+ var inherited = group_actions(perms.inherited);
+
+ /* Inheritable set may not be specified in the same terms as the
+ * action_map so we need to infer it.
+ * Example: we may have read|modify leaving delete unspecified.
+ * We treat this as read|modify|-delete
+ */
+ for (var role in inherited) {
+ var agroups = inherited[role];
+ for (var group in agroups) {
+ var actions = agroups[group];
+ var highest = null;
+ for (var i in actions) {
+ var act = actions[i];
+ if (act.charAt(0) == '-') {
+ continue;;
+ }
+ if (highest == null || rank[group][act] > highest) {
+ highest = rank[group][act];
+ }
+ }
+ if (highest == null) {
+ delete inherited[role][group];
+ continue;
+ }
+ // Compute full value
+ var comp = [];
+ for (var act in rank[group]) {
+ var val = rank[group][act];
+ if (val <= highest) {
+ comp.push(act);
+ } else {
+ comp.push('-' + act);
+ }
+ }
+ inherited[role][group] = comp.join('|');
+ }
+ }
+
+ function add_acl_entity(role)
+ {
+ // Delete role from select box
+ $('option', sel).each(function () {
+ if ($(this).attr('value') == role) {
+ $(this).remove();
+ }
+ });
+ // Create a row for this role
+ var sp = $('<tr style="cursor:pointer"/>');
+ var label = $('<span/>');
+ label.text(groups[role]);
+ label.attr('data-role', role);
+ sp.append(
+ $('<td/>')
+ .html('<span style="position: absolute; margin-left: -1.3em" class="ui-icon ui-icon-arrowthick-2-n-s"></span>')
+ .append(label)
+ );
+ tbody.append(sp);
+
+ for (var gi in cat_order) {
+ var group = cat_order[gi];
+ var gsel = $('<select/>');
+ gsel.data('acl.role', role);
+ var data = mobj[group];
+ for (var i in data) {
+ var a = data[i];
+ gsel.append(
+ $('<option/>')
+ .attr('value', a[0])
+ .text(a[1])
+ );
+ }
+ if (roledefs[role]) {
+ gsel.val(roledefs[role][group].join('|'));
+ }
+ sp.append(
+ $('<td/>')
+ .append(gsel)
+ );
+ }
+ var b = $('<button class="btn btn-mini"><i class="icon-trash"></i></button>');
+ sp.append(
+ $('<td/>')
+ .append(b)
+ );
+ b.click(function () {
+ sp.remove();
+ sel.append(
+ $('<option/>')
+ .attr('value', role)
+ .text(groups[role])
+ );
+ });
+ }
+
+ // Add fixed inherited rows
+ var thead = $('thead', $(this.el));
+ for (var role in inherited) {
+ tr = $('<tr class="inheritedacl"/>');
+ tr.append($('<td/>').text(groups[role]));
+ for (var group in mobj) {
+ var d = inherited[role][group];
+ if (d) {
+ // Good old fashioned look up (we don't have this hashed)
+ for (var i in mobj[group]) {
+ var ent = mobj[group][i];
+ if (ent[0] == d) {
+ d = ent[1];
+ break;
+ }
+ }
+ tr.append($('<td/>').text(d));
+ } else {
+ tr.append($('<td>(Not Specified)</td>'));
+ }
+ }
+ thead.append(tr);
+ }
+ sel = $('<select/>');
+ sel.append(
+ $('<option/>')
+ .text('Add...')
+ );
+
+ // Populate list of roles and expand titles
+ $.ajax({
+ url: ABSWEB + "api.php/acl/roles",
+ success: function (data) {
+ // Update any labels and expand the list
+ // with data from the server side
+ for (var id in data) {
+ var label = data[id];
+
+ groups[id] = label;
+ }
+
+ // Update any labels that may be in the table.
+ // While looking, build up a list of who is in the table;
+ // we don't want to add those people back to the "Add..."
+ // option at the bottom.
+ var already = {};
+ $('span[data-role]', this.el).each(function () {
+ var sp = $(this);
+ var role = sp.attr('data-role');
+ already[role] = role;
+ sp.text(groups[role]);
+ });
+
+ // Now add the users to "Add..."
+ for (var i in groups) {
+ if (i in already) continue;
+ var g = groups[i];
+ sel.append(
+ $('<option/>')
+ .attr('value', i)
+ .text(g)
+ );
+ }
+ },
+ });
+
+ $(this.el).append(sel);
+
+ /* make the tbody sortable. Note that we append the "Add..." to the table,
+ * not the tbody, so that we don't allow dragging it around */
+ tbody.sortable();
+
+ for (var role in roledefs) {
+ add_acl_entity(role);
+ }
+
+ sel.change(function () {
+ var v = sel.val();
+ if (v && v.length) {
+ add_acl_entity(v);
+ }
+ });
+
+ return this;
+ },
+ compute: function() {
+ var acl = [];
+ var tbody = $('tbody', this.el);
+ $('select', tbody).each(function () {
+ var role = $(this).data('acl.role');
+ var val = $(this).val().split('|');
+ for (var i in val) {
+ var action = val[i];
+ var allow = 1;
+ if (action.substring(0, 1) == '-') {
+ allow = 0;
+ action = action.substring(1);
+ }
+ acl.push([role, action, allow]);
+ }
+ });
+ /* suitable for doing: perms.acl = V.compute();
+ * and feeding back to compatible REST APIs */
+ return acl;
+ }
+});
+
MTrackRepoEditView = Backbone.View.extend({
initialize: function(options) {
this.options = options;
@@ -2152,6 +2461,26 @@
$(view.el).remove();
});

+ var acleditor = null;
+ if (!view.model.isNew()) {
+ acleditor = new MTrackACLEditView({
+ model: view.model,
+ el: $('#repo-perms', dlg),
+ action_map: {
+ Web: {
+ read: 'Browse via web UI',
+ modify: 'Administer via web UI',
+ delete: 'Delete repo via web UI'
+ },
+ SSH: {
+ checkout: 'Check-out repo via SSH',
+ commit: 'Commit changes to repo via SSH'
+ }
+ }
+ });
+ acleditor.render();
+ }
+
/* get the list of eligible projects for notifications;
* when it arrives, update the notifications tab */
view.projects = new MTrackProjectCollection;
@@ -2281,6 +2610,12 @@
data.scmtype = typ;
}

+ if (acleditor) {
+ data.perms = {
+ acl: acleditor.compute()
+ };
+ }
+
view.model.save(data, {
success: function (model, resp) {
/* do something useful */



https://bitbucket.org/wez/mtrack/changeset/f2995f3e35a6/
changeset: f2995f3e35a6
user: wez
date: 2012-04-23 06:41:04
summary: beef up project editor to include permissions and groups
affected #: 8 files

diff -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 -r f2995f3e35a63f536d345c0d31b352f7e818cb05 inc/auth/http.php
--- a/inc/auth/http.php
+++ b/inc/auth/http.php
@@ -289,7 +289,9 @@
function enumGroups() {
if (strlen($this->htgroup)) {
list($groups, $users) = $this->readGroupFile($this->htgroup);
- return array_keys($groups);
+ if (is_array($groups)) {
+ return array_keys($groups);
+ }
}
return null;
}


diff -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 -r f2995f3e35a63f536d345c0d31b352f7e818cb05 inc/issue.php
--- a/inc/issue.php
+++ b/inc/issue.php
@@ -493,6 +493,26 @@
return $reason;
}

+ function getGroups() {
+ $res = new stdclass;
+ foreach (MTrackDB::q(<<<SQL
+select g.name as groupname, m.username as user
+from groups g
+ left join group_membership m on
+ (g.name = m.groupname AND g.project = m.project)
+where g.project = ?
+SQL
+ , $this->projid)->fetchAll(PDO::FETCH_OBJ) as $ent) {
+ if (!isset($res->{$ent->groupname})) {
+ $res->{$ent->groupname} = array();
+ }
+ if ($ent->user) {
+ $res->{$ent->groupname}[] = $ent->user;
+ }
+ }
+ return $res;
+ }
+
static function rest_project_apply($in, MTrackProject $P, MTrackChangeset $CS)
{
error_log(json_encode($in));
@@ -508,6 +528,25 @@
if (isset($in->notifyemail)) {
$P->notifyemail = $in->notifyemail;
}
+ if (isset($in->perms) && isset($in->perms->acl)) {
+ MTrackACL::setACL("project:$P->projid", 0, $in->perms->acl);
+ }
+ if (isset($in->groups) && $P->projid) {
+ MTrackDB::q('delete from group_membership where project = ?', $P->projid);
+ MTrackDB::q('delete from groups where project = ?', $P->projid);
+
+ foreach ($in->groups as $grpname => $users) {
+ MTrackDB::q('insert into groups (name, project) values (?, ?)',
+ $grpname, $P->projid);
+ if (is_array($users)) {
+ foreach ($users as $user) {
+ MTrackDB::q('insert into group_membership (groupname, project, username) values (?, ?, ?)',
+ $grpname, $P->projid, $user);
+ }
+ }
+ }
+
+ }

// TODO: component association
}
@@ -518,6 +557,12 @@

// TODO: component association

+ if (MTrackACL::hasAnyRights("project:$P->projid", 'modify')) {
+ $p->perms = MTrackACL::computeACLObject("project:$P->projid");
+ }
+
+ $p->groups = $P->getGroups();
+
return $p;
}

@@ -527,9 +572,14 @@
if ($method == 'GET') {
MTrackACL::requireAllRights('Projects', 'read');

- return MTrackDB::q(
- 'select projid as id, name, shortname, ordinal, notifyemail
- from projects order by ordinal')->fetchAll(PDO::FETCH_OBJ);
+ $res = array();
+ foreach (MTrackDB::q(
+ 'select projid as id from projects order by ordinal')
+ ->fetchAll(PDO::FETCH_COLUMN, 0) as $pid) {
+ $P = self::loadById($pid);
+ $res[] = self::rest_project_return($P);
+ }
+ return $res;
}
MTrackACL::requireAllRights('Projects', 'create');

@@ -565,6 +615,9 @@
if (!is_object($in)) {
MTrackAPI::error(400, "expected json payload");
}
+ if (isset($in->shortname) && $in->shortname != $P->shortname) {
+ MTrackAPI::error(400, "cannot change shortname");
+ }
$CS = MTrackChangeset::begin("project:$P->projid",
"Edit project $in->name");
self::rest_project_apply($in, $P, $CS);
@@ -1890,6 +1943,19 @@
return $matches;
}

+ static function rest_active_users($method, $uri, $captures) {
+ MTrackAPI::checkAllowed($method, 'GET');
+ /* avoid leaking information to anonymous users */
+ $me = MTrackAuth::whoami();
+ if ($me == 'anonymous' || MTrackAuth::getUserClass() == 'anonymous') {
+ return array();
+ }
+ $tkt = new MTrackIssue;
+ $users = $tkt->_determineAssigneeList(array(), true);
+
+ return $users;
+ }
+
static function rest_ticket_cclist($method, $uri, $captures) {
MTrackAPI::checkAllowed($method, 'GET');
/* avoid leaking information to anonymous users */
@@ -2600,6 +2666,7 @@
MTrackAPI::register('/ticket', 'MTrackIssue::rest_ticket_new');
MTrackAPI::register('/ticket/meta/fields', 'MTrackIssue::rest_ticket_fields');
MTrackAPI::register('/ticket/meta/cc', 'MTrackIssue::rest_ticket_cclist');
+MTrackAPI::register('/ticket/meta/users', 'MTrackIssue::rest_active_users');
MTrackAPI::register('/ticket/search/basic', 'MTrackIssue::rest_ticket_search');
MTrackLink::register('ticket', 'MTrackIssue::resolve_link');
MTrackLink::register('comment', 'MTrackIssue::resolve_comment_link');


diff -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 -r f2995f3e35a63f536d345c0d31b352f7e818cb05 inc/web.php
--- a/inc/web.php
+++ b/inc/web.php
@@ -1132,7 +1132,7 @@
global $ABSWEB;

$cat_titles = array(
- 'user' => 'User Administration &amp; Authentication',
+ 'user' => 'Users &amp; Groups',
'repo' => 'Configure Repositories',
'projects' => 'Projects &amp; Notifications',
'tickets' => 'Configure Tickets',
@@ -1144,7 +1144,7 @@
$add_cat = '_mtrack_admin_nav_add_cat';

if (MTrackACL::hasAnyRights('Projects', 'modify')) {
- $add_cat($by_cat, "<a href='{$ABSWEB}admin/project.php'><i class='icon-envelope'></i> Projects and their notification settings</a>", 'projects');
+ $add_cat($by_cat, "<a href='{$ABSWEB}admin/project.php'><i class='icon-envelope'></i> Projects and Groups</a>", 'user');
}

if (MTrackACL::hasAnyRights('Enumerations', 'modify')) {
@@ -1168,7 +1168,6 @@
if (MTrackACL::hasAllRights('User', 'modify')) {
$add_cat($by_cat, "<a href='{$ABSWEB}admin/auth.php'><i class='icon-lock'></i> Authentication</a>", 'logs');
$add_cat($by_cat, "<a href='{$ABSWEB}admin/user.php'><i class='icon-user'></i> Users</a>", 'user');
- $add_cat($by_cat, "<a href='{$ABSWEB}admin/group.php'><i class='icon-user'></i> Groups</a>", 'user');
}

if (MTrackACL::hasAnyRights('Tickets', 'create')) {


diff -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 -r f2995f3e35a63f536d345c0d31b352f7e818cb05 web/admin/group.php
--- a/web/admin/group.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php # vim:ts=2:sw=2:et:
-/* For licensing and copyright terms, see the file named LICENSE */
-include '../../inc/common.php';
-
-if (!isset($_REQUEST['pid'])) {
- throw new Exception("missing project id");
-}
-$pid = (int)$_REQUEST['pid'];
-
-MTrackACL::requireAnyRights("project:$pid", 'modify');
-
-$P = MTrackProject::loadById($pid);
-if (!$P) {
- throw new Exception("invalid project " . htmlentities($pid));
-}
-
-if (isset($_REQUEST['group'])) {
- $group = $_REQUEST['group'];
-} else {
- $group = null;
-}
-
-if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- if (!strlen($group)) {
- throw new Exception("missing group name");
- }
- if (isset($_POST['members'])) {
- $members = $_POST['members'];
- } else {
- $members = array();
- }
-
- $CS = MTrackChangeset::begin("project:$pid", "Changed group $group");
- if (isset($_POST['isnew'])) {
- MTrackDB::q('insert into groups (name, project) values (?, ?)',
- $group, $pid);
- }
-
- MTrackDB::q(
- 'delete from group_membership where groupname = ? and project = ?',
- $group, $pid);
- foreach ($members as $username) {
- MTrackDB::q(
- 'insert into group_membership (groupname, project, username) values (?,?,?)',
- $group, $pid, $username);
- }
- $CS->commit();
- header("Location: {$ABSWEB}admin/project.php?edit=$pid");
- exit;
-}
-
-mtrack_head($group ? "$P->name - $group" : "$P->name - New Group");
-mtrack_admin_nav();
-
-echo "<form method='post'><input type='hidden' name='pid' value='$pid'>";
-if ($group) {
- echo "<h1>" . htmlentities("$P->name - $group", ENT_QUOTES, 'utf-8') . "</h1>";
- echo "<input type='hidden' name='group' value='" .
- htmlentities($group, ENT_QUOTES, 'utf-8') .
- "'>";
-} else {
- echo "<h1>" . htmlentities("$P->name - New Group", ENT_QUOTES, 'utf-8') . "</h1>";
- echo "Group: <input type='text' name='group'>";
- echo "<input type='hidden' name='isnew' value='1'>";
-}
-
-$users = array();
-foreach (MTrackDB::q('select userid, fullname from userinfo where active = 1 order by userid')
- ->fetchAll() as $row) {
- if (strlen($row[1])) {
- $disp = "$row[0] - $row[1]";
- } else {
- $disp = $row[0];
- }
- $users[$row[0]] = $disp;
-}
-$members = array();
-foreach (MTrackDB::q('select username from group_membership where project = ? and groupname = ?', $pid, $group)->fetchAll(PDO::FETCH_COLUMN, 0) as $name) {
- $members[$name] = $name;
-}
-echo mtrack_multi_select_box('members', "Members", $users, $members);
-
-echo "<input type='submit' value='Save'>";
-
-echo "</form>";
-
-mtrack_foot();
-


diff -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 -r f2995f3e35a63f536d345c0d31b352f7e818cb05 web/admin/project.php
--- a/web/admin/project.php
+++ b/web/admin/project.php
@@ -23,47 +23,16 @@
// $('#projlist').sortable();

function show_edit(P) {
- var dlg = $('#editdialog');
- $('input[name=shortname]', dlg).val(P.get('shortname'));
- $('input[name=name]', dlg).val(P.get('name'));
- $('input[name=notifyemail]', dlg).val(P.get('notifyemail'));
-
+ var V = new MTrackProjectEditView({
+ model: P
+ });
var is_new = P.isNew();
-
- dlg.modal('show');
-
- $('#saveproj').on('click.saveproj', function () {
- P.save({
- shortname: $('input[name=shortname]', dlg).val(),
- name: $('input[name=name]', dlg).val(),
- notifyemail: $('input[name=notifyemail]', dlg).val()
- },{
- success: function (model, resp) {
- dlg.modal('hide');
- if (is_new) {
- projects.add(model, {at: projects.length});
- } else {
- render_list();
- }
- },
- error: function (model, resp) {
- var err;
- if (!_.isObject(resp)) {
- err = resp;
- } else {
- err = resp.statusText;
- try {
- var r = JSON.parse(resp.responseText);
- err = r.message;
- } catch (e) {
- }
- }
- $('<div class="alert alert-danger">' +
- "<a class='close' data-dismiss='alert'>&times;</a>" +
- err + '</div>').
- appendTo('#editdialog div.modal-body');
- }
- });
+ V.show(function (model) {
+ if (is_new) {
+ projects.add(model, {at: projects.length});
+ } else {
+ render_list();
+ }
});
};

@@ -108,32 +77,6 @@
</script><button id='newrepobtn' class='btn btn-primary'
type='button'><i class='icon-plus icon-white'></i> New Project</button>
-<div id='editdialog' class='modal hide'>
- <div class='modal-header'>
- <a class='close' data-dismiss='modal'>&times;</a>
- <h3>Edit Project</h3>
- </div>
- <div class='modal-body'>
- <p>
- Short name:<br>
- <input type='text' name='shortname'>
- </p>
- <p>
- Descriptive name:<br>
- <input type='text' name='name'>
- </p>
- <p>
- Send Notification Email to:<br>
- <input type='text' name='notifyemail'>
- </p>
- </div>
- <div class='modal-footer'>
- <button class='btn' data-dismiss='modal'>Cancel</button>
- <button id='saveproj'
- class='btn btn-primary'>Save Project</button>
- </div>
- </form>
-</div><ul id='projlist' class='nav nav-pills nav-stacked'></ul>


diff -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 -r f2995f3e35a63f536d345c0d31b352f7e818cb05 web/js/models.js
--- a/web/js/models.js
+++ b/web/js/models.js
@@ -26,6 +26,12 @@
}

var MTrackProject = Backbone.Model.extend({
+ defaults: {
+ "shortname": '',
+ "name": '',
+ "notifyemail": ''
+ },
+
url: function() {
if (this.isNew()) {
return ABSWEB + "api.php/project";


diff -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 -r f2995f3e35a63f536d345c0d31b352f7e818cb05 web/js/templates/project.edit.html
--- /dev/null
+++ b/web/js/templates/project.edit.html
@@ -0,0 +1,79 @@
+<div class='modal hide repoeditor'>
+ <div class='modal-header'>
+ <a class='close' data-dismiss='modal'>&times;</a>
+ <% if (isnew) { %>
+ <h3>Create Project</h3>
+ <% } else { %>
+ <h3>Edit Project <%- shortname %></h3>
+ <% } %>
+ </div>
+ <div class='modal-body'>
+
+ <ul class="nav nav-tabs">
+ <li class='active'><a href='#project-main' data-toggle='tab'>Details</a></li>
+ <li><a href='#project-groups' data-toggle='tab'>Groups</a></li>
+ <li><a href='#project-perms' data-toggle='tab'>Permissions</a></li>
+ </ul>
+ <div class='tab-content'>
+ <div class='tab-pane active' id='project-main'>
+ <% if (isnew) { %>
+ <p>
+ Short name:<br>
+ <input type='text' name='shortname'
+ placeholder="Enter shortname; choose wisely, it cannot be changed!"
+ value="<%- shortname %>"
+ >
+ </p>
+ <% } %>
+ <p>
+ Descriptive name:<br>
+ <input type='text' name='name'
+ placeholder="Enter descriptive name"
+ value="<%- name %>"
+ >
+ </p>
+ <p>
+ Send Notification Email to:<br>
+ <input type='text' name='notifyemail'
+ placeholder="Enter an email address"
+ value="<%- notifyemail %>"
+ >
+ </p>
+ </div>
+
+ <div class='tab-pane' id='project-groups'>
+ <% if (isnew) { %>
+ <p>Save this project to enable group editing</p>
+ <% } else { %>
+ <div class='tabbable tabs-left'
+ style='overflow:auto; height:19em;'>
+ <ul class='nav nav-tabs'>
+ </ul>
+ <div class='tab-content' style='width:auto'>
+ </div>
+ </div>
+ <hr>
+ <input type='text' name='newgroup'
+ placeholder='Enter new group name'>
+ <button id='addgroup' class='btn'
+ ><i class='icon-plus'></i> Add Group</button>
+ <% } %>
+
+ </div>
+
+ <div class='tab-pane' id='project-perms'>
+ <% if (isnew) { %>
+ <p>Save this project to enable permission editing</p>
+ <% } %>
+ </div>
+ </div>
+
+ </div>
+ <div class='modal-footer'>
+ <button class='btn' data-dismiss='modal'>Cancel</button>
+ <button id='saveproj'
+ class='btn btn-primary'>Save Project</button>
+ </div>
+ </form>
+</div>
+


diff -r 7231ee6052f7c916fd66f442e1ce06e67dfc11c9 -r f2995f3e35a63f536d345c0d31b352f7e818cb05 web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -2441,8 +2441,6 @@
initialize: function(options) {
this.options = options;
this.template = _.template(mtrack_underscore_templates['repo-edit']);
-// this.link_template = _.template(
-// mtrack_underscore_templates['repo-edit-link']);
},
show: function(on_success) {
var o = this.model.toJSON();
@@ -2633,3 +2631,169 @@

});

+MTrackProjectEditView = Backbone.View.extend({
+ initialize: function(options) {
+ this.options = options;
+ this.template = _.template(mtrack_underscore_templates['project-edit']);
+ },
+ show: function(on_success) {
+ var o = this.model.toJSON();
+ if (!o.name) {
+ o.name = '';
+ }
+ if (!o.notifyemail) {
+ o.notifyemail = '';
+ }
+ o.isnew = this.model.isNew();
+
+ $(this.el).html(this.template(o));
+
+ var view = this;
+ $(view.el).appendTo('body');
+ var dlg = $('div.modal', view.el);
+ dlg.on('hidden', function () {
+ $(view.el).remove();
+ });
+
+ var acleditor = null;
+ var userlist = null;
+ var proto_select = null;
+
+ function make_group_tab(grpname, activate) {
+ if (!grpname || !grpname.match(/\S/)) return null;
+ $('input[name=newgroup]', dlg).val('');
+
+ /* already have a tab with that name? */
+ var tab = null;
+ $('#project-groups ul.nav-tabs li', dlg).each(function () {
+ if ($(this).attr('data-name') == grpname) {
+ tab = $(this);
+ }
+ });
+
+ if (tab) return tab;
+
+ tab = $('<li/>');
+ tab.attr('data-name', grpname);
+ var a = $('<a/>');
+ a.attr('href', '#group-' + grpname);
+ a.attr('data-toggle', 'tab');
+ a.text(grpname);
+ tab.append(a);
+ $('#project-groups ul.nav-tabs', dlg).append(tab);
+
+ /* also need to create the list of people */
+ var plist = $('<div class="tab-pane"/>');
+ plist.attr('id', 'group-' + grpname);
+ plist.text("Group: " + grpname);
+ plist.append("<br/>");
+
+ $('#project-groups div.tab-content', dlg).append(plist);
+
+ var sel = proto_select.clone();
+ sel.data('group-name', grpname);
+ plist.append(sel);
+ var groups = view.model.get('groups');
+ sel.val(groups[grpname]);
+ sel.css('width', '300px').chosen();
+
+ if (activate) {
+ // Activate new tab
+ // would do: a.tab(); but it doesn't seem to work
+ a.trigger('click');
+ }
+
+ return tab;
+ }
+
+ if (!view.model.isNew()) {
+ acleditor = new MTrackACLEditView({
+ model: view.model,
+ el: $('#project-perms', dlg),
+ action_map: {
+ Admin: {
+ modify: 'Administer via web UI'
+ }
+ }
+ });
+ acleditor.render();
+
+ $.ajax({
+ url: ABSWEB + "api.php/ticket/meta/users",
+ success: function (data) {
+ userlist = data;
+
+ proto_select = $('<select/>');
+ proto_select.attr('multiple', 'multiple');
+ _.each(userlist, function (user) {
+ var opt = $('<option/>');
+ opt.attr('value', user.id);
+ opt.text(user.label);
+ proto_select.append(opt);
+ });
+
+ // now we can render the group tabs
+ var groups = view.model.get('groups');
+ _.each(groups, function (users, grpname) {
+ make_group_tab(grpname);
+ });
+ }
+ });
+ }
+
+ /* adding groups */
+ $('button#addgroup', dlg).click(function () {
+ var grpname = $('input[name=newgroup]', dlg).val();
+ console.log("grpname is", grpname);
+ var tab = make_group_tab(grpname, true);
+ });
+
+ /* save button */
+ $('button.btn-primary', dlg).click(function () {
+ /* get fields out */
+ var name = $('input[name=name]', dlg).val();
+ var notifyemail = $('input[name=notifyemail]', dlg).val();
+
+ var data = {
+ name: name,
+ notifyemail: notifyemail
+ };
+
+ if (view.model.isNew()) {
+ var shortname = $('input[name=shortname]', dlg).val();
+ data.shortname = shortname;
+ }
+
+ if (acleditor) {
+ data.perms = {
+ acl: acleditor.compute()
+ };
+
+ /* compute the groups */
+ var groups = {};
+ $('#project-groups select', dlg).each(function () {
+ var sel = $(this);
+ var grpname = sel.data('group-name');
+ groups[grpname] = sel.val();
+ });
+ console.log("save", groups);
+ data.groups = groups;
+ }
+
+ view.model.save(data, {
+ success: function (model, resp) {
+ /* do something useful */
+ dlg.modal('hide');
+ on_success(model);
+ },
+ error: function (model, resp) {
+ mtrack_ajax_error_to_dom(resp, $('div.modal-body', dlg));
+ }
+ });
+ });
+
+ dlg.modal('show');
+ }
+
+});
+



https://bitbucket.org/wez/mtrack/changeset/230c7682b572/
changeset: 230c7682b572
user: wez
date: 2012-04-23 06:55:06
summary: don't generate dom id's with spaces in them
affected #: 1 file

diff -r f2995f3e35a63f536d345c0d31b352f7e818cb05 -r 230c7682b57252b004d1beb68e3999e62583a8c7 web/js/views.js
--- a/web/js/views.js
+++ b/web/js/views.js
@@ -2658,6 +2658,7 @@
var acleditor = null;
var userlist = null;
var proto_select = null;
+ var next_group_id = 1;

function make_group_tab(grpname, activate) {
if (!grpname || !grpname.match(/\S/)) return null;
@@ -2676,7 +2677,8 @@
tab = $('<li/>');
tab.attr('data-name', grpname);
var a = $('<a/>');
- a.attr('href', '#group-' + grpname);
+ var grpid = 'group-' + next_group_id++;
+ a.attr('href', '#' + grpid);
a.attr('data-toggle', 'tab');
a.text(grpname);
tab.append(a);
@@ -2684,7 +2686,7 @@

/* also need to create the list of people */
var plist = $('<div class="tab-pane"/>');
- plist.attr('id', 'group-' + grpname);
+ plist.attr('id', grpid);
plist.text("Group: " + grpname);
plist.append("<br/>");

@@ -2744,7 +2746,6 @@
/* adding groups */
$('button#addgroup', dlg).click(function () {
var grpname = $('input[name=newgroup]', dlg).val();
- console.log("grpname is", grpname);
var tab = make_group_tab(grpname, true);
});

@@ -2776,7 +2777,6 @@
var grpname = sel.data('group-name');
groups[grpname] = sel.val();
});
- console.log("save", groups);
data.groups = groups;
}

Repository URL: https://bitbucket.org/wez/mtrack/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
Reply all
Reply to author
Forward
0 new messages