add stemma edit/add dialog, textinfo edit dialog, UI bugfixes mostly to error handlin...
Tara L Andrews [Sun, 2 Sep 2012 00:40:35 +0000 (02:40 +0200)]
lib/stemmaweb/Controller/Root.pm
root/css/style.css
root/js/componentload.js
root/src/index.tt

index 639d7c1..494f5c1 100644 (file)
@@ -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;
 }
 
index 0717f8a..a90f064 100644 (file)
@@ -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;
index 02cd0fc..25ff0aa 100644 (file)
@@ -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
index 5c8c239..b3cdb32 100644 (file)
@@ -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>