From: Tara L Andrews <tla@mit.edu>
Date: Sun, 2 Sep 2012 00:40:35 +0000 (+0200)
Subject: add stemma edit/add dialog, textinfo edit dialog, UI bugfixes mostly to error handlin... 
X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=75354c3abbba7d3146b2c676009d8b614ab8699a;p=scpubgit%2Fstemmaweb.git

add stemma edit/add dialog, textinfo edit dialog, UI bugfixes mostly to error handling; still TODO stemma display after edit
---

diff --git a/lib/stemmaweb/Controller/Root.pm b/lib/stemmaweb/Controller/Root.pm
index 639d7c1..494f5c1 100644
--- a/lib/stemmaweb/Controller/Root.pm
+++ b/lib/stemmaweb/Controller/Root.pm
@@ -66,230 +66,302 @@ sub directory :Local :Args(0) {
 	$c->stash->{template} = 'directory.tt';
 }
 
-=head2 variantgraph
+=head1 AJAX methods for traditions and their properties
 
- GET /variantgraph/$textid
+=head2 newtradition
+
+ POST /newtradition,
+ 	{ name: <name>,
+ 	  language: <language>,
+ 	  public: <is_public>,
+ 	  file: <fileupload> }
  
-Returns the variant graph for the text specified at $textid, in SVG form.
+Creates a new tradition belonging to the logged-in user, with the given name
+and the collation given in the uploaded file. The file type is indicated via
+the filename extension (.csv, .txt, .xls, .xlsx, .xml). Returns the ID and 
+name of the new tradition.
+ 
+=cut
+
+sub newtradition :Local :Args(0) {
+	my( $self, $c ) = @_;
+	return _json_error( $c, 403, 'Cannot save a tradition without being logged in' )
+		unless $c->user_exists;
+
+	my $user = $c->user->get_object;
+	# Grab the file upload, check its name/extension, and call the
+	# appropriate parser(s).
+	my $upload = $c->request->upload('file');
+	my $name = $c->request->param('name') || 'Uploaded tradition';
+	my $lang = $c->request->param( 'language' ) || 'Default';
+	my $public = $c->request->param( 'public' ) ? 1 : undef;
+	my( $ext ) = $upload->filename =~ /\.(\w+)$/;
+	my %newopts = (
+		'name' => $name,
+		'language' => $lang,
+		'public' => $public,
+		'file' => $upload->tempname
+		);
+
+	my $tradition;
+	my $errmsg;
+	if( $ext eq 'xml' ) {
+		# Try the different XML parsing options to see if one works.
+		foreach my $type ( qw/ CollateX CTE TEI / ) {
+			try {
+				$tradition = Text::Tradition->new( %newopts, 'input' => $type );
+			} catch ( Text::Tradition::Error $e ) {
+				$errmsg = $e->message;
+			} catch {
+				$errmsg = "Unexpected parsing error";
+			}
+			last if $tradition;
+		}
+	} elsif( $ext =~ /^(txt|csv|xls(x)?)$/ ) {
+		# If it's Excel we need to pass excel => $ext;
+		# otherwise we need to pass sep_char => [record separator].
+		if( $ext =~ /xls/ ) {
+			$newopts{'excel'} = $ext;
+		} else {
+			$newopts{'sep_char'} = $ext eq 'txt' ? "\t" : ',';
+		}
+		try {
+			$tradition = Text::Tradition->new( 
+				%newopts,
+				'input' => 'Tabular',
+				);
+		} catch ( Text::Tradition::Error $e ) {
+			$errmsg = $e->message;
+		} catch {
+			$errmsg = "Unexpected parsing error";
+		}
+	} else {
+		# Error unless we have a recognized filename extension
+		return _json_error( $c, 500, "Unrecognized file type extension $ext" );
+	}
+	
+	# Save the tradition if we have it, and return its data or else the
+	# error that occurred trying to make it.
+	if( $errmsg ) {
+		return _json_error( $c, 500, "Error parsing tradition .$ext file: $errmsg" );
+	} elsif( !$tradition ) {
+		return _json_error( $c, 500, "No error caught but tradition not created" );
+	}
+
+	my $m = $c->model('Directory');
+	$user->add_tradition( $tradition );
+	my $id = $c->model('Directory')->store( $tradition );
+	$c->model('Directory')->store( $user );
+	$c->stash->{'result'} = { 'id' => $id, 'name' => $tradition->name };
+	$c->forward('View::JSON');
+}
+
+=head2 textinfo
+
+ GET /textinfo/$textid
+ POST /textinfo/$textid, 
+ 	{ name: $new_name, 
+ 	  language: $new_language,
+ 	  public: $is_public, 
+ 	  owner: $new_userid } # only admin users can update the owner
+ 
+Returns information about a particular text.
 
 =cut
 
-sub variantgraph :Local :Args(1) {
+sub textinfo :Local :Args(1) {
 	my( $self, $c, $textid ) = @_;
 	my $tradition = $c->model('Directory')->tradition( $textid );
+	unless( $tradition ) {
+		return _json_error( $c, 500, "No tradition with ID $textid" );
+	}	
 	my $ok = _check_permission( $c, $tradition );
 	return unless $ok;
+	if( $c->req->method eq 'POST' ) {
+		return _json_error( $c, 403, 
+			'You do not have permission to update this tradition' ) 
+			unless $ok eq 'full';
+		my $params = $c->request->parameters;
+		# Handle changes to owner-accessible parameters
+		my $m = $c->model('Directory');
+		my $changed;
+		# Handle scalar params
+		foreach my $param ( qw/ name language / ) {
+			if( exists $params->{$param} ) {
+				my $newval = delete $params->{$param};
+				unless( $tradition->$param eq $newval ) {
+					try {
+						$tradition->$param( $newval );
+					} catch {
+						return _json_error( $c, 500, "Error setting $param to $newval" );
+					}
+					$changed = 1;
+				}
+			}
+		}
+		# Handle our boolean
+		if( delete $params->{'public'} ) {  # if it's any true value...
+			$tradition->public( 1 );
+		}
+		# Handle ownership changes
+		my $newuser;
+		if( exists $params->{'owner'} ) {
+			# Only admins can update user / owner
+			my $newownerid = delete $params->{'owner'};
+			unless( $tradition->has_user && $tradition->user->id eq $newownerid ) {
+				unless( $c->user->get_object->is_admin ) {
+					return _json_error( $c, 403, 
+						"Only admin users can change tradition ownership" );
+				}
+				$newuser = $m->lookup_user( $params->{'owner'} );
+				unless( $newuser ) {
+					return _json_error( $c, 500, "No such user " . $params->{'owner'} );
+				}
+				$newuser->add_tradition( $tradition );
+				$changed = 1;
+			}
+		}
+		# TODO check for rogue parameters
+		if( scalar keys %$params ) {
+			my $rogueparams = join( ', ', keys %$params );
+			return _json_error( $c, 403, "Request parameters $rogueparams not recognized" );
+		}
+		# If we safely got to the end, then write to the database.
+		$m->save( $tradition ) if $changed;
+		$m->save( $newuser ) if $newuser;		
+	}
 
-	my $collation = $tradition->collation;
-	$c->stash->{'result'} = $collation->as_svg;
-	$c->forward('View::SVG');
+	# Now return the current textinfo, whether GET or successful POST.
+	my $textinfo = {
+		textid => $textid,
+		name => $tradition->name,
+		language => $tradition->language,
+		public => $tradition->public,
+		owner => $tradition->user ? $tradition->user->id : undef,
+		witnesses => [ map { $_->sigil } $tradition->witnesses ],
+	};
+	my @stemmasvg = map { $_->as_svg({ size => [ 500, 375 ] }) } $tradition->stemmata;
+	map { $_ =~ s/\n/ /mg } @stemmasvg;
+	$textinfo->{stemmata} = \@stemmasvg;
+	$c->stash->{'result'} = $textinfo;
+	$c->forward('View::JSON');
 }
-	
-=head2 alignment
 
- GET /alignment/$textid
+=head2 variantgraph
 
-Returns an alignment table for the text specified at $textid.
+ GET /variantgraph/$textid
+ 
+Returns the variant graph for the text specified at $textid, in SVG form.
 
 =cut
 
-sub alignment :Local :Args(1) {
+sub variantgraph :Local :Args(1) {
 	my( $self, $c, $textid ) = @_;
 	my $tradition = $c->model('Directory')->tradition( $textid );
+	unless( $tradition ) {
+		return _json_error( $c, 500, "No tradition with ID $textid" );
+	}	
 	my $ok = _check_permission( $c, $tradition );
 	return unless $ok;
 
 	my $collation = $tradition->collation;
-	my $alignment = $collation->alignment_table;
-	
-	# Turn the table, so that witnesses are by column and the rows
-	# are by rank.
-	my $wits = [ map { $_->{'witness'} } @{$alignment->{'alignment'}} ];
-	my $rows;
-	foreach my $i ( 0 .. $alignment->{'length'} - 1 ) {
-		my @rankrdgs = map { $_->{'tokens'}->[$i]->{'t'} } 
-			@{$alignment->{'alignment'}};
-		push( @$rows, { 'rank' => $i+1, 'readings' => \@rankrdgs } );
-	}
-	$c->stash->{'witnesses'} = $wits;
-	$c->stash->{'table'} = $rows;
-	$c->stash->{'template'} = 'alignment.tt';
+	$c->stash->{'result'} = $collation->as_svg;
+	$c->forward('View::SVG');
 }
-
+	
 =head2 stemma
 
- GET /stemma/$textid/$stemmaid
- POST /stemma/$textid, { 'dot' => $dot_string }
+ GET /stemma/$textid/$stemmaseq
+ POST /stemma/$textid/$stemmaseq, { 'dot' => $dot_string }
 
-Returns an SVG representation of the stemma hypothesis for the text.  If 
-the URL is called with POST and a new dot string, updates the stemma and
-returns the SVG as with GET.
+Returns an SVG representation of the given stemma hypothesis for the text.  
+If the URL is called with POST, the stemma at $stemmaseq will be altered
+to reflect the definition in $dot_string. If $stemmaseq is 'n', a new
+stemma will be added.
 
 =cut
 
-sub stemma :Local :Args {
+sub stemma :Local :Args(2) {
 	my( $self, $c, $textid, $stemmaid ) = @_;
 	my $m = $c->model('Directory');
 	my $tradition = $m->tradition( $textid );
+	unless( $tradition ) {
+		return _json_error( $c, 500, "No tradition with ID $textid" );
+	}	
 	my $ok = _check_permission( $c, $tradition );
 	return unless $ok;
 
-	$stemmaid = 0 unless defined $stemmaid;
 	$c->stash->{'result'} = '';
-	if( $tradition ) {
-		if( $c->req->method eq 'POST' ) {
-			# Update the stemma
+	my $stemma;
+	if( $c->req->method eq 'POST' ) {
+		if( $ok eq 'full' ) {
 			my $dot = $c->request->body_params->{'dot'};
-			$tradition->add_stemma( $dot );
+			try {
+				if( $stemmaid eq 'n' ) {
+					# We are adding a new stemma.
+					$stemma = $tradition->add_stemma( 'dot' => $dot );
+				} elsif( $stemmaid < $tradition->stemma_count ) {
+					# We are updating an existing stemma.
+					$stemma = $tradition->stemma( $stemmaid );
+					$stemma->alter_graph( $dot );
+				} else {
+					# Unrecognized stemma ID
+					return _json_error( $c, 500, "No stemma at index $stemmaid, cannot update" );
+				}
+			} catch ( Text::Tradition::Error $e ) {
+				return _json_error( $c, 500, $e->message );
+			}
 			$m->store( $tradition );
-			$stemmaid = scalar( $tradition->stemma_count ) - 1;
+		} else {
+			# No permissions to update the stemma
+			return _json_error( $c, 403, 
+				'You do not have permission to update stemmata for this tradition' );
 		}
-		
-		$c->stash->{'result'} = $tradition->stemma_count > $stemmaid
-			? $tradition->stemma( $stemmaid )->as_svg( { size => [ 500, 375 ] } )
-			: '';
 	}
+	
+	# For a GET or a successful POST request, return the SVG representation
+	# of the stemma in question, if any.
+	$c->log->debug( "Received Accept header: " . $c->req->header('Accept') );
+	if( !$stemma && $tradition->stemma_count > $stemmaid ) {
+		$stemma = $tradition->stemma( $stemmaid );
+	}
+	$c->stash->{'result'} = $stemma 
+		? $stemma->as_svg( { size => [ 500, 375 ] } ) : '';
 	$c->forward('View::SVG');
 }
 
 =head2 stemmadot
 
- GET /stemmadot/$textid
+ GET /stemmadot/$textid/$stemmaseq
  
 Returns the 'dot' format representation of the current stemma hypothesis.
 
 =cut
 
-sub stemmadot :Local :Args(1) {
-	my( $self, $c, $textid ) = @_;
+sub stemmadot :Local :Args(2) {
+	my( $self, $c, $textid, $stemmaid ) = @_;
 	my $m = $c->model('Directory');
 	my $tradition = $m->tradition( $textid );
+	unless( $tradition ) {
+		return _json_error( $c, 500, "No tradition with ID $textid" );
+	}	
 	my $ok = _check_permission( $c, $tradition );
 	return unless $ok;
-	
-	$c->response->body( $tradition->stemma->editable );
-	$c->forward('View::Plain');
-}
-
-=head1 AJAX methods for index page
-
-=head2 textinfo
-
- GET /textinfo/$textid
- 
-Returns information about a particular text.
-
-=cut
-
-sub textinfo :Local :Args(1) {
-	my( $self, $c, $textid ) = @_;
-	my $tradition = $c->model('Directory')->tradition( $textid );
-	my $ok = _check_permission( $c, $tradition );
-	return unless $ok;
-
-	# Need text name, witness list, scalar readings, scalar relationships, stemmata
-	my $textinfo = {
-		textid => $textid,
-		traditionname => $tradition->name,
-		witnesses => [ map { $_->sigil } $tradition->witnesses ],
-		readings => scalar $tradition->collation->readings,
-		relationships => scalar $tradition->collation->relationships
-	};
-	my @stemmasvg = map { $_->as_svg({ size => [ 500, 375 ] }) } $tradition->stemmata;
-	map { $_ =~ s/\n/ /mg } @stemmasvg;
-	$textinfo->{stemmata} = \@stemmasvg;
-	$c->stash->{'result'} = $textinfo;
+	my $stemma = $tradition->stemma( $stemmaid );
+	unless( $stemma ) {
+		return _json_error( $c, 500, "Tradition $textid has no stemma ID $stemmaid" );
+	}
+	# Get the dot and transmute its line breaks to literal '|n'
+	$c->stash->{'result'} = { 'dot' =>  $stemma->editable( { linesep => '|n' } ) };
 	$c->forward('View::JSON');
 }
 
-# TODO alter text parameters
+####################
+### Helper functions
+####################
 
-=head2 new
-
- POST /newtradition { name: <name>, inputfile: <fileupload> }
- 
-Creates a new tradition belonging to the logged-in user, according to the detected
-file type. Returns the ID and name of the new tradition.
- 
-=cut
-
-sub newtradition :Local :Args(0) {
-	my( $self, $c ) = @_;
-	if( $c->user_exists ) {
-		my $user = $c->user->get_object;
-		# Grab the file upload, check its name/extension, and call the
-		# appropriate parser(s).
-		my $upload = $c->request->upload('file');
-		my $name = $c->request->param('name') || 'Uploaded tradition';
-		my( $ext ) = $upload->filename =~ /\.(\w+)$/;
-		my %newopts = (
-			'name' => $name,
-			'file' => $upload->tempname
-			);
-		my $tradition;
-		my $errmsg;
-		if( $ext eq 'xml' ) {
-			# Try the different XML parsing options to see if one works.
-			foreach my $type ( qw/ CollateX CTE TEI / ) {
-				try {
-					$tradition = Text::Tradition->new( %newopts, 'input' => $type );
-				} catch ( Text::Tradition::Error $e ) {
-					$errmsg = $e->message;
-				} catch {
-					$errmsg = "Unexpected parsing error";
-				}
-				last if $tradition;
-			}
-		} elsif( $ext =~ /^(txt|csv|xls(x)?)$/ ) {
-			# If it's Excel we need to pass excel => $ext;
-			# otherwise we need to pass sep_char => [record separator].
-			if( $ext =~ /xls/ ) {
-				$newopts{'excel'} = $ext;
-			} else {
-				$newopts{'sep_char'} = $ext eq 'txt' ? "\t" : ',';
-			}
-			try {
-				$tradition = Text::Tradition->new( 
-					%newopts,
-					'input' => 'Tabular',
-					);
-			} catch ( Text::Tradition::Error $e ) {
-				$errmsg = $e->message;
-			} catch {
-				$errmsg = "Unexpected parsing error";
-			}
-		} elsif( $ext eq 'xlsx' ) {
-			$c->stash->{'result'} = 
-				{ 'error' => "Excel XML parsing not supported yet" };
-			$c->response->status( 500 );
-		} else {
-			# Error unless we have a recognized filename extension
-			$c->stash->{'result'} = 
-				{ 'error' => "Unrecognized file type extension $ext" };
-			$c->response->status( 500 );
-		}
-		
-		# Save the tradition if we have it, and return its data or else the
-		# error that occurred trying to make it.
-		if( $tradition ) {
-			my $m = $c->model('Directory');
-			$user->add_tradition( $tradition );
-			my $id = $c->model('Directory')->store( $tradition );
-			$c->model('Directory')->store( $user );
-			$c->stash->{'result'} = { 'id' => $id, 'name' => $tradition->name };
-		} else {
-			$c->stash->{'result'} = 
-				{ 'error' => "Error parsing tradition .$ext file: $errmsg" };
-			$c->response->status( 500 );
-		}
-	} else {
-		$c->stash->{'result'} = 
-			{ 'error' => 'Cannot save a tradition without being logged in' };
-		$c->response->status( 403 );
-	}
-	$c->forward('View::JSON');
-}
- 
+# Helper to check what permission, if any, the active user has for
+# the given tradition
 sub _check_permission {
 	my( $c, $tradition ) = @_;
     my $user = $c->user_exists ? $c->user->get_object : undef;
@@ -301,9 +373,15 @@ sub _check_permission {
 	return 'readonly' if $tradition->public;
 
 	# ...nope. Forbidden!
-	$c->response->status( 403 );
-	$c->response->body( 'You do not have permission to view this tradition.' );
-	$c->detach( 'View::Plain' );
+	return _json_error( $c, 403, 'You do not have permission to view this tradition.' );
+}
+
+# Helper to throw a JSON exception
+sub _json_error {
+	my( $c, $code, $errmsg ) = @_;
+	$c->response->status( $code );
+	$c->stash->{'result'} = { 'error' => $errmsg };
+	$c->forward('View::JSON');
 	return 0;
 }
 
diff --git a/root/css/style.css b/root/css/style.css
index 0717f8a..a90f064 100644
--- a/root/css/style.css
+++ b/root/css/style.css
@@ -109,6 +109,10 @@ div.button:hover span {
 .mainnav a {
 	color: #488dd2;
 }
+#textinfo_waitbox {
+	float: left;
+	padding-left: 50px;
+}
 #textinfo_container {
     border: 1px solid #C6DCF1;
 	float: left;
@@ -118,6 +122,9 @@ div.button:hover span {
 	padding-left: 10px;
 	padding-right: 10px;
 }
+#edit_instructions {
+	float: left;
+}
 #stemma_container h2 h3 {
 	color: #666;
 }
@@ -139,7 +146,7 @@ div.button:hover span {
 #textinfo_container_buttons {
     float: right;
 }
-#run_stexaminer, #run_relater, #stemma_pager, #add_edit_stemma {
+#run_stexaminer, #run_relater, #open_stemma_add, #open_stemma_edit, #stemma_pager, #open_textinfo_edit {
     height: 30px;
     left: -10px;
     position: relative;
diff --git a/root/js/componentload.js b/root/js/componentload.js
index 02cd0fc..25ff0aa 100644
--- a/root/js/componentload.js
+++ b/root/js/componentload.js
@@ -1,37 +1,37 @@
 function loadTradition( textid, textname, editable ) {
 	selectedTextID = textid;
     // First insert the placeholder image and register an error handler
-    var basepath = window.location.pathname
-    if( basepath.lastIndexOf('/') == basepath.length - 1 ) { 
-    	basepath = basepath.slice( 0, basepath.length - 1) 
-    };
     $('#textinfo_load_status').empty();
     $('#stemma_graph').empty();
     $('#textinfo_waitbox').show();
-    $('#textinfo_container').ajaxError( 
-    	function ( e, jqxhr, settings, exception ) {
-			if ( settings.url.indexOf( 'textinfo' ) > -1 ) {
-		    	$('#textinfo_waitbox').hide();
-				var msg = "An error occurred: ";
-				var msghtml = $('<span>').attr('class', 'error').text(
-					msg + jqxhr.status + " " + jqxhr.statusText);
-				$("#textinfo_load_status").append( msghtml ).show();
-			} 
+    $('#textinfo_container').hide().ajaxError( 
+    	function(event, jqXHR, ajaxSettings, thrownError) {
+    	if( ajaxSettings.url.indexOf( 'textinfo' ) > -1 && ajaxSettings.type == 'GET'  ) {
+			$('#textinfo_waitbox').hide();
+			$('#textinfo_container').show();
+			display_error( jqXHR, $("#textinfo_load_status") );
     	}
-    );
+    });
+    
+    // Hide the functionality that is irrelevant
+    if( editable ) {
+    	$('#add_new_stemma').show();
+    	$('#edit_current_stemma').show();
+    	$('#edit_textinfo').show();
+    } else {
+    	$('#add_new_stemma').hide();
+    	$('#edit_current_stemma').hide();
+    	$('#edit_textinfo').hide();
+    }
+
     // Then get and load the actual content.
     // TODO: scale #stemma_graph both horizontally and vertically
     // TODO: load svgs from SVG.Jquery (to make scaling react in Safari)
     $.getJSON( basepath + "/textinfo/" + textid, function (textdata) {
     	// Add the scalar data
-    	$('#textinfo_waitbox').hide();
-		$('#textinfo_container').show();
-    	$('.texttitle').empty().append( textdata.traditionname );
-    	$('#witness_num').empty().append( textdata.witnesses.size );
-    	$('#witness_list').empty().append( textdata.witnesses.join( ', ' ) );
-    	$('#reading_num').empty().append( textdata.readings );
-    	$('#relationship_num').empty().append( textdata.relationships );
-    	// Add the stemma(ta) and set up the stexaminer button
+    	selectedTextInfo = textdata;
+    	load_textinfo();
+     	// Add the stemma(ta) and set up the stexaminer button
     	stemmata = textdata.stemmata;
     	if( stemmata.length ) {
     		selectedStemmaID = 0;
@@ -42,7 +42,32 @@ function loadTradition( textid, textname, editable ) {
 	});
 }
 
-function load_stemma( idx, basepath ) {
+function load_textinfo() {
+	$('#textinfo_waitbox').hide();
+	$('#textinfo_load_status').empty();
+	$('#textinfo_container').show();
+	$('.texttitle').empty().append( selectedTextInfo.name );
+	// Witnesses
+	$('#witness_num').empty().append( selectedTextInfo.witnesses.size );
+	$('#witness_list').empty().append( selectedTextInfo.witnesses.join( ', ' ) );
+	// Who the owner is
+	$('#owner_id').empty().append('no one');
+	if( selectedTextInfo.owner ) {
+		$('#owner_id').empty().append( selectedTextInfo.owner );
+	}
+	// Whether or not it is public
+	$('#not_public').empty();
+	if( selectedTextInfo['public'] == false ) {
+		$('#not_public').append('NOT ');
+	}
+	// What language setting it has, if any
+	$('#marked_language').empty().append('no language set');
+	if( selectedTextInfo.language && selectedTextInfo.language != 'Default' ) {
+		$('#marked_language').empty().append( selectedTextInfo.language );
+	}
+}	
+
+function load_stemma( idx ) {
 	if( idx > -1 ) {
 		selectedStemmaID = idx;
 		$('#stemma_graph').empty();
@@ -52,3 +77,15 @@ function load_stemma( idx, basepath ) {
 		$('#run_stexaminer').attr( 'action', stexpath );
 	}
 }
+
+function display_error( jqXHR, el ) {
+	var errobj = jQuery.parseJSON( jqXHR.responseText );
+	var msg;
+	if( errobj ) {
+		msg = "An error occurred: " + errobj.error;
+	} else {
+		msg = "An error occurred; perhaps the server went down?"
+	}
+	var msghtml = $('<span>').attr('class', 'error').text( msg );
+	$(el).empty().append( msghtml ).show();
+}
\ No newline at end of file
diff --git a/root/src/index.tt b/root/src/index.tt
index 5c8c239..b3cdb32 100644
--- a/root/src/index.tt
+++ b/root/src/index.tt
@@ -3,7 +3,12 @@
 	applicationjs = c.uri_for( 'js/componentload.js' )
 %]
     <script type="text/javascript">
+var basepath = window.location.pathname
+if( basepath.lastIndexOf('/') == basepath.length - 1 ) { 
+	basepath = basepath.slice( 0, basepath.length - 1) 
+};
 var selectedTextID;
+var selectedTextInfo;
 var selectedStemmaID = -1;
 var stemmata = [];
 
@@ -33,6 +38,121 @@ $(document).ready( function() {
     $('#textinfo_container').hide();
     $('#textinfo_waitbox').hide();
 	refreshDirectory();
+	
+	// Set up the textinfo edit dialog
+	$('#textinfo-edit-dialog').dialog({
+		autoOpen: false,
+		height: 200,
+		width: 300,
+		modal: true,
+		buttons: {
+			Save: function (evt) {
+				$(evt.target).button("disable");
+				var requrl = "[% c.uri_for( '/textinfo' ) %]/" + selectedTextID;
+				var reqparam = $('#edit_textinfo').serialize();
+				$.post( requrl, reqparam, function (data) {
+					// Reload the selected text fields
+					selectedTextInfo = data;
+					load_textinfo();
+					// Reenable the button and close the form
+					$(evt.target).button("enable");
+					$('#textinfo-edit-dialog').dialog('close');
+				}, 'json' );
+			},
+			Cancel: function() {
+				$('#textinfo-edit-dialog').dialog('close');
+			}
+		},
+		open: function() {
+			$("#edit_textinfo_status").empty();
+			// Populate the form fields with the current values
+			// edit_(name, language, public, owner)
+			$.each([ 'name', 'language', 'owner' ], function( idx, k ) {
+				var fname = '#edit_' + k;
+				$(fname).val( selectedTextInfo[k] );
+			});
+			if( selectedTextInfo['public'] == true ) {
+				$('#edit_public').attr('checked','true');
+			} else {
+				$('#edit_public').removeAttr('checked');
+			}
+		},
+	}).ajaxError( function(event, jqXHR, ajaxSettings, thrownError) {
+		$(event.target).parent().find('.ui-button').button("enable");
+    	if( ajaxSettings.url.indexOf( 'textinfo' ) > -1 
+    		&& ajaxSettings.type == 'POST' ) {
+			display_error( jqXHR, $("#edit_textinfo_status") );
+    	}
+	});
+
+	
+	// Set up the stemma editor dialog
+	$('#stemma-edit-dialog').dialog({
+		autoOpen: false,
+		height: 700,
+		width: 600,
+		modal: true,
+		buttons: {
+			Save: function (evt) {
+				$(evt.target).button("disable");
+				var stemmaseq = $('#stemmaseq').val();
+				var requrl = "[% c.uri_for( '/stemma' ) %]/" + selectedTextID + "/" + stemmaseq;
+				var reqparam = { 'dot': $('#dot_field').val() };
+				// TODO We need to stash the literal SVG string in stemmata
+				// somehow. Implement accept header on server side to decide
+				// whether to send application/json or application/xml?
+				$.post( requrl, reqparam, function (data) {
+					// We received a stemma SVG string in return. 
+					// Update the current stemma sequence number
+					if( stemmaseq == 'n' ) {
+						selectedStemmaID = stemmata.length;
+					} else {
+						selectedStemmaID = stemmaseq;
+					}
+					// Strip the carriage returns from the answer
+					var newsvg = data.replace(/(\r\n|\n|\r)/gm," ");
+					// Stash the answer in our SVG array
+					stemmata[selectedStemmaID] = newsvg;
+					// Display the new stemma
+					load_stemma( selectedStemmaID );
+					// Reenable the button and close the form
+					$(evt.target).button("disable");
+					$('#stemma-edit-dialog').dialog('close');
+				}, 'xml' );
+			},
+			Cancel: function() {
+				$('#stemma-edit-dialog').dialog('close');
+			}
+		},
+		open: function(evt) {
+			$("#edit_stemma_status").empty();
+			var stemmaseq = $('#stemmaseq').val();
+			if( stemmaseq == 'n' ) {
+				// If we are creating a new stemma, populate the textarea with a
+				// bare digraph.
+				$(evt.target).dialog('option', 'title', 'Add a new stemma')
+				$('#dot_field').val( "digraph stemma {\n\n}" );
+			} else {
+				// If we are editing a stemma, grab its stemmadot and populate the
+				// textarea with that.
+				$(evt.target).dialog('option', 'title', 'Edit selected stemma')
+				$('#dot_field').val( 'Loading, please wait...' );
+				var doturl = "[% c.uri_for( '/stemmadot' ) %]/" + selectedTextID + "/" + stemmaseq;
+				$.getJSON( doturl, function (data) {
+					// Re-insert the line breaks
+					var dotstring = data.dot.replace(/\|n/gm, "\n");					
+					$('#dot_field').val( dotstring );
+				});
+			}
+		},
+	}).ajaxError( function(event, jqXHR, ajaxSettings, thrownError) {
+		$(event.target).parent().find('.ui-button').button("enable");
+    	if( ajaxSettings.url.indexOf( 'stemma' ) > -1 
+    		&& ajaxSettings.type == 'POST' ) {
+			display_error( jqXHR, $("#edit_stemma_status") );
+    	}
+	});
+		
 	$('#upload-collation-dialog').dialog({
 		autoOpen: false,
 		height: 325,
@@ -53,7 +173,7 @@ $(document).ready( function() {
                 return false;
             }
 		  },
-		  cancel: function() {
+		  Cancel: function() {
 		    $('#upload-collation-dialog').dialog('close');
 		  }
 		},
@@ -81,24 +201,44 @@ $(document).ready( function() {
 [% END %]
     </div>
     <div id="textinfo_waitbox">
-    	<img src="[% c.uri_for( 'images', 'ajax-loader.gif' ) %]" alt="Loading tradition info..."/>
+    	<h3>Loading tradition information, please wait...</h3>
+    	<img src="[% c.uri_for( 'images', 'ajax-loader.gif' ) %]" alt="Loading tradition info..." />
     </div>
     <div id="textinfo_container">
       <div id="textinfo_load_status"></div>
       <h2>Text <span class="texttitle"></span></h2>
       <ul>
+      	  <li>is owned by <span id="owner_id"></span></li>
+      	  <li>is <span id="not_public"></span>public</li>
+      	  <li>has <span id="marked_language"></span> as its primary language</li>
 	      <li>has <span id="witness_num"></span> witnesses: <span id="witness_list"></span></li>
-	      <li>has <span id="reading_num"></span> distinct readings</li>
-	      <li>has <span id="relationship_num"></span> distinct reading relationships</li>
       </ul>
       
       <!-- TODO buttons on either side of the graph div to flip through the stemmata -->
       <div id="textinfo_container_buttons">
+          <form id="open_textinfo_edit" action="" method="GET" name="edit_textinfo">
+            <div class="button" id="edit_textinfo_button"
+            	onClick="$('#textinfo-edit-dialog').dialog('open')">
+          	  <span>Modify information about this tradition</span>
+            </div>
+          </form>
           <form id="stemma_pager" action="" method="GET" name="stemma_pager">
             <div class="button" id="stemma_pager_button" onClick="$('#stemma_pager').submit()">
           	  <span>Left &amp; right go here</span>
             </div>
           </form>
+          <form id="open_stemma_add" action="" method="GET" name="add_new_stemma">
+            <div class="button" id="stemma_add_button" 
+            	onClick="$('#stemmaseq').val('n'); $('#stemma-edit-dialog').dialog('open');">
+          	  <span>Add a new stemma</span>
+            </div>
+          </form>
+          <form id="open_stemma_edit" action="" method="GET" name="edit_current_stemma">
+            <div class="button" id="stemma_edit_button" 
+            	onClick="$('#stemmaseq').val(selectedStemmaID); $('#stemma-edit-dialog').dialog('open');">
+          	  <span>Edit this stemma</span>
+            </div>
+          </form>
           <form id="run_stexaminer" action="" method="GET" name="run_stexaminer">
             <div class="button" id="stexaminer_button" onClick="$('#run_stexaminer').submit()">
           	  <span>Examine variants against this stemma</span>
@@ -116,15 +256,71 @@ $(document).ready( function() {
     <!-- Interim 'loading' message for directory box -->
     <div id="loading_message">
     	<h3>Loading texts, please wait...</h3>
-    	<img src="[% c.uri_for( 'images', 'ajax-loader.gif' ) %]" />
+    	<img src="[% c.uri_for( 'images', 'ajax-loader.gif' ) %]" alt="Loading tradition list..."/>
+    </div>
+    
+    <!-- Textinfo editor dialog -->
+    <div id="textinfo-edit-dialog" title="Edit information about this tradition">
+      <div id="textinfo_edit_container">
+    	<form id="edit_textinfo">
+    		<label for="edit_name">Tradition name: </label>
+    		<input id="edit_name" type="text" size="30" name="name"/><br/>
+    		<label for="edit_language">Language: </label>
+    		<input id="edit_language" type="text" size="12" name="language"/>
+    		<label for="edit_public">Publicly viewable: </label>
+    		<input id="edit_public" type="checkbox" name="public"/><br/>
+[% IF c.user_exists -%]
+[% IF c.user.get_object.is_admin -%]
+    		<label for="edit_owner">Publicly viewable: </label>
+    		<input id="edit_owner" type="text" size="30" name="owner"/><br/>
+[% END -%]
+[% END -%]
+		</form>
+		<div id="edit_textinfo_status"></div>
+	  </div>
+    </div>
+    
+    <!-- Stemma dot editor dialog, simple textarea for now -->
+    <div id="stemma-edit-dialog">
+      <div id="stemma_edit_container">
+    	<form id="edit_stemma">
+    		<label for="dot_field">Dot definition for this stemma: </label><br/>
+    		<textarea id="dot_field" rows="30" cols="40"></textarea>
+    		<input id="stemmaseq" type="hidden" name="stemmaseq" val="n"/>
+			<div id="edit_instructions">
+				<p>All definitions begin with the line
+					<pre>digraph stemma {</pre>
+				and end with the line 
+					<pre>}</pre>Please do not change these lines.</p>
+				<p>First list each witness in your stemma, whether extant or lost /
+				reconstructed / hypothetical, and assign them a class of either "extant"
+				or "hypothetical". For example:</p><pre>  
+	α [ class=hypothetical ]
+	C [ class=extant ]
+				</pre>
+				<p>Next, list the direct links between witnesses, one per line. For example, if 
+				witness C descends directly from witness α, note it as follows:</p><pre>
+	α -> C
+				</pre>
+				<p>A witness may be the exemplar for any number of other witnesses, whether 
+				extant or not; likewise, a witness may inherit from any number of other 
+				witnesses. Use as may "A -> B" pairings as necessary to describe the links.</p>
+			</div>
+    	</form>
+    	<div id="edit_stemma_status"></div>
+      </div>
     </div>
 
     <!-- File upload dialog box -->
     <div id="upload-collation-dialog" title="Upload a collation">
       <div id="upload_container">
         <form id="new_tradition">
-            <label for="traditionname">Name of this text / tradition: </label>
-            <input id="traditionname" type="text" name="name" size="40"/><br/>
+            <label for="new_name">Name of this text / tradition: </label>
+            <input id="new_name" type="text" name="name" size="40"/><br/>
+            <label for="new_lang">Primary language of the text: </label>
+            <input id="new_lang" type="text" name="language" size="20"/><br/>
+            <label for="new_public">Allow public display: </label>
+            <input id="new_public" name="public" type="checkbox"/><br/>
             <div id="filelist"></div>
         <form>
         <div id="upload_status"></div>