allow relationship colors in SVG download; work around inability to close Download...
[scpubgit/stemmaweb.git] / lib / stemmaweb / Controller / Root.pm
index 070f7d8..c772122 100644 (file)
@@ -1,8 +1,10 @@
 package stemmaweb::Controller::Root;
 use Moose;
 use namespace::autoclean;
-use Text::Tradition::Analysis qw/ run_analysis /;
+use JSON qw ();
 use TryCatch;
+use XML::LibXML;
+use XML::LibXML::XPathContext;
 
 
 BEGIN { extends 'Catalyst::Controller' }
@@ -40,6 +42,28 @@ sub index :Path :Args(0) {
     $c->stash->{template} = 'index.tt';
 }
 
+=head2 about
+
+A general overview/documentation page for the site.
+
+=cut
+
+sub about :Local :Args(0) {
+       my( $self, $c ) = @_;
+       $c->stash->{template} = 'about.tt';
+}
+
+=head2 help/*
+
+A dispatcher for documentation of various aspects of the application.
+
+=cut
+
+sub help :Local :Args(1) {
+       my( $self, $c, $topic ) = @_;
+       $c->stash->{template} = "$topic.tt";
+}
+
 =head1 Elements of index page
 
 =head2 directory
@@ -110,16 +134,41 @@ sub newtradition :Local :Args(0) {
        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 / ) {
+               my $type;
+               # Parse the XML to see which flavor it is.
+               my $parser = XML::LibXML->new();
+               my $doc;
+               try {
+                       $doc = $parser->parse_file( $newopts{'file'} );
+               } catch( $err ) {
+                       $errmsg = "XML file parsing error: $err";
+               }
+               if( $doc ) {
+                       if( $doc->documentElement->nodeName eq 'graphml' ) {
+                               $type = 'CollateX';
+                       } elsif( $doc->documentElement->nodeName ne 'TEI' ) {
+                               $errmsg = 'Unrecognized XML type ' . $doc->documentElement->nodeName;
+                       } else {
+                               my $xpc = XML::LibXML::XPathContext->new( $doc->documentElement );
+                               my $venc = $xpc->findvalue( '/TEI/teiHeader/encodingDesc/variantEncoding/attribute::method' );
+                               if( $venc && $venc eq 'double-end-point' ) {
+                                       $type = 'CTE';
+                               } else {
+                                       $type = 'TEI';
+                               }
+                       }
+               }
+               # Try the relevant XML parsing option.
+               if( $type ) {
+                       delete $newopts{'file'};
+                       $newopts{'xmlobj'} = $doc;
                        try {
                                $tradition = Text::Tradition->new( %newopts, 'input' => $type );
                        } catch ( Text::Tradition::Error $e ) {
                                $errmsg = $e->message;
-                       } catch {
-                               $errmsg = "Unexpected parsing error";
+                       } catch ( $e ) {
+                               $errmsg = "Unexpected parsing error: $e";
                        }
-                       last if $tradition;
                }
        } elsif( $ext =~ /^(txt|csv|xls(x)?)$/ ) {
                # If it's Excel we need to pass excel => $ext;
@@ -136,12 +185,12 @@ sub newtradition :Local :Args(0) {
                                );
                } catch ( Text::Tradition::Error $e ) {
                        $errmsg = $e->message;
-               } catch {
-                       $errmsg = "Unexpected parsing error";
+               } catch ( $e ) {
+                       $errmsg = "Unexpected parsing error: $e";
                }
        } else {
                # Error unless we have a recognized filename extension
-               return _json_error( $c, 500, "Unrecognized file type extension $ext" );
+               return _json_error( $c, 403, "Unrecognized file type extension $ext" );
        }
        
        # Save the tradition if we have it, and return its data or else the
@@ -176,8 +225,11 @@ Returns information about a particular text.
 sub textinfo :Local :Args(1) {
        my( $self, $c, $textid ) = @_;
        my $tradition = $c->model('Directory')->tradition( $textid );
+       ## Have to keep users in the same scope as tradition
+       my $newuser;
+       my $olduser;
        unless( $tradition ) {
-               return _json_error( $c, 500, "No tradition with ID $textid" );
+               return _json_error( $c, 404, "No tradition with ID $textid" );
        }       
        my $ok = _check_permission( $c, $tradition );
        return unless $ok;
@@ -197,18 +249,19 @@ sub textinfo :Local :Args(1) {
                                        $tradition->name( $newname );
                                        $changed = 1;
                                } catch {
-                                       return _json_error( $c, 500, "Error setting name to $newname" );
+                                       return _json_error( $c, 500, "Error setting name to $newname: $@" );
                                }
                        }
                }
                # Handle language param, making Default => null
                my $langval = delete $params->{language} || 'Default';
-               unless( $tradition->language eq $langval ) {
+               
+               unless( $tradition->language eq $langval || !$tradition->can('language') ) {
                        try {
                                $tradition->language( $langval );
                                $changed = 1;
                        } catch {
-                               return _json_error( $c, 500, "Error setting language to $langval" );
+                               return _json_error( $c, 500, "Error setting language to $langval: $@" );
                        }
                }
 
@@ -223,20 +276,26 @@ sub textinfo :Local :Args(1) {
                }
                
                # Handle ownership change
-               my $newuser;
                if( exists $params->{'owner'} ) {
                        # Only admins can update user / owner
                        my $newownerid = delete $params->{'owner'};
+                       if( $tradition->has_user && !$tradition->user ) {
+                               $tradition->clear_user;
+                       }
                        unless( !$newownerid || 
-                               ( $tradition->has_user && $tradition->user->id eq $newownerid ) ) {
+                               ( $tradition->has_user && $tradition->user->email eq $newownerid ) ) {
                                unless( $c->user->get_object->is_admin ) {
                                        return _json_error( $c, 403, 
                                                "Only admin users can change tradition ownership" );
                                }
-                               $newuser = $m->find_user({ username => $newownerid });
+                               $newuser = $m->find_user({ email => $newownerid });
                                unless( $newuser ) {
                                        return _json_error( $c, 500, "No such user " . $newownerid );
                                }
+                               if( $tradition->has_user ) {
+                                       $olduser = $tradition->user;
+                                       $olduser->remove_tradition( $tradition );
+                               }
                                $newuser->add_tradition( $tradition );
                                $changed = 1;
                        }
@@ -255,13 +314,18 @@ sub textinfo :Local :Args(1) {
        my $textinfo = {
                textid => $textid,
                name => $tradition->name,
-               language => $tradition->language,
-               public => $tradition->public,
-               owner => $tradition->user ? $tradition->user->id : undef,
+               public => $tradition->public || 0,
+               owner => $tradition->user ? $tradition->user->email : undef,
                witnesses => [ map { $_->sigil } $tradition->witnesses ],
        };
-       my @stemmasvg = map { $_->as_svg({ size => [ 500, 375 ] }) } $tradition->stemmata;
-       map { $_ =~ s/\n/ /mg } @stemmasvg;
+       ## TODO Make these into callbacks in the other controllers maybe?
+       if( $tradition->can('language') ) {
+               $textinfo->{'language'} = $tradition->language;
+       }
+       if( $tradition->can('stemweb_jobid') ) {
+               $textinfo->{'stemweb_jobid'} = $tradition->stemweb_jobid || 0;
+       }
+       my @stemmasvg = map { _stemma_info( $_ ) } $tradition->stemmata;
        $textinfo->{stemmata} = \@stemmasvg;
        $c->stash->{'result'} = $textinfo;
        $c->forward('View::JSON');
@@ -279,7 +343,7 @@ 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" );
+               return _json_error( $c, 404, "No tradition with ID $textid" );
        }       
        my $ok = _check_permission( $c, $tradition );
        return unless $ok;
@@ -288,6 +352,22 @@ sub variantgraph :Local :Args(1) {
        $c->stash->{'result'} = $collation->as_svg;
        $c->forward('View::SVG');
 }
+
+sub _stemma_info {
+       my( $stemma, $sid ) = @_;
+       my $ssvg = $stemma->as_svg();
+       $ssvg =~ s/\n/ /mg;
+       my $sinfo = {
+               name => $stemma->identifier, 
+               directed => _json_bool( !$stemma->is_undirected ),
+               svg => $ssvg }; 
+       if( $sid ) {
+               $sinfo->{stemmaid} = $sid;
+       }
+       return $sinfo;
+}
+
+## TODO Separate stemma manipulation functionality into its own controller.
        
 =head2 stemma
 
@@ -306,7 +386,7 @@ sub stemma :Local :Args(2) {
        my $m = $c->model('Directory');
        my $tradition = $m->tradition( $textid );
        unless( $tradition ) {
-               return _json_error( $c, 500, "No tradition with ID $textid" );
+               return _json_error( $c, 404, "No tradition with ID $textid" );
        }       
        my $ok = _check_permission( $c, $tradition );
        return unless $ok;
@@ -321,13 +401,15 @@ sub stemma :Local :Args(2) {
                                        # We are adding a new stemma.
                                        $stemmaid = $tradition->stemma_count;
                                        $stemma = $tradition->add_stemma( 'dot' => $dot );
+                               } elsif( $stemmaid !~ /^\d+$/ ) {
+                                       return _json_error( $c, 403, "Invalid stemma ID specification $stemmaid" );
                                } 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" );
+                                       return _json_error( $c, 404, "No stemma at index $stemmaid, cannot update" );
                                }
                        } catch ( Text::Tradition::Error $e ) {
                                return _json_error( $c, 500, $e->message );
@@ -345,7 +427,6 @@ sub stemma :Local :Args(2) {
        if( !$stemma && $tradition->stemma_count > $stemmaid ) {
                $stemma = $tradition->stemma( $stemmaid );
        }
-       my $stemma_xml = $stemma ? $stemma->as_svg( { size => [ 500, 375 ] } ) : '';
        # What was requested, XML or JSON?
        my $return_view = 'SVG';
        if( my $accept_header = $c->req->header('Accept') ) {
@@ -361,11 +442,10 @@ sub stemma :Local :Args(2) {
                }
        }
        if( $return_view eq 'SVG' ) {
-               $c->stash->{'result'} = $stemma_xml;
+               $c->stash->{'result'} = $stemma->as_svg();
                $c->forward('View::SVG');
        } else { # JSON
-               $stemma_xml =~ s/\n/ /mg;
-               $c->stash->{'result'} = { 'stemmaid' => $stemmaid, 'stemmasvg' => $stemma_xml };
+               $c->stash->{'result'} = { _stemma_info( $stemma, $stemmaid ) };
                $c->forward('View::JSON');
        }
 }
@@ -383,19 +463,91 @@ sub stemmadot :Local :Args(2) {
        my $m = $c->model('Directory');
        my $tradition = $m->tradition( $textid );
        unless( $tradition ) {
-               return _json_error( $c, 500, "No tradition with ID $textid" );
+               return _json_error( $c, 404, "No tradition with ID $textid" );
        }       
        my $ok = _check_permission( $c, $tradition );
        return unless $ok;
        my $stemma = $tradition->stemma( $stemmaid );
        unless( $stemma ) {
-               return _json_error( $c, 500, "Tradition $textid has no stemma ID $stemmaid" );
+               return _json_error( $c, 404, "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');
 }
 
+=head2 stemmaroot
+
+ POST /stemmaroot/$textid/$stemmaseq, { root: <root node ID> }
+
+Orients the given stemma so that the given node is the root (archetype). Returns the 
+information structure for the new stemma.
+
+=cut 
+
+sub stemmaroot :Local :Args(2) {
+       my( $self, $c, $textid, $stemmaid ) = @_;
+       my $m = $c->model('Directory');
+       my $tradition = $m->tradition( $textid );
+       unless( $tradition ) {
+               return _json_error( $c, 404, "No tradition with ID $textid" );
+       }       
+       my $ok = _check_permission( $c, $tradition );
+       if( $ok eq 'full' ) {
+               my $stemma = $tradition->stemma( $stemmaid );
+               try {
+                       $stemma->root_graph( $c->req->param('root') );
+                       $m->save( $tradition );
+               } catch( Text::Tradition::Error $e ) {
+                       return _json_error( $c, 400, $e->message );
+               } catch {
+                       return _json_error( $c, 500, "Error re-rooting stemma: $@" );
+               }
+               $c->stash->{'result'} = _stemma_info( $stemma );
+               $c->forward('View::JSON');
+       } else {
+               return _json_error( $c, 403,  
+                               'You do not have permission to update stemmata for this tradition' );
+       }
+}
+
+=head2 download
+
+ GET /download/$textid/$format
+Returns a file for download of the tradition in the requested format.
+=cut
+
+sub download :Local :Args(2) {
+       my( $self, $c, $textid, $format ) = @_;
+       my $tradition = $c->model('Directory')->tradition( $textid );
+       unless( $tradition ) {
+               return _json_error( $c, 404, "No tradition with ID $textid" );
+       }
+       my $ok = _check_permission( $c, $tradition );
+       return unless $ok;
+
+       my $outmethod = "as_" . lc( $format );
+       my $view = "View::$format";
+       $c->stash->{'name'} = $tradition->name();
+       $c->stash->{'download'} = 1;
+       my @outputargs;
+       if( $format eq 'SVG' ) {
+               # Send the list of colors through to the backend.
+               # TODO Think of some way not to hard-code this.
+               push( @outputargs, { 'show_relations' => 'all',
+                       'graphcolors' => [ "#5CCCCC", "#67E667", "#F9FE72", "#6B90D4", 
+                               "#FF7673", "#E467B3", "#AA67D5", "#8370D8", "#FFC173" ] } );
+       }
+       try {
+               $c->stash->{'result'} = $tradition->collation->$outmethod( @outputargs );
+       } catch( Text::Tradition::Error $e ) {
+               return _json_error( $c, 500, $e->message );
+       }
+       $c->forward( $view );
+}
+
 ####################
 ### Helper functions
 ####################
@@ -425,6 +577,10 @@ sub _json_error {
        return 0;
 }
 
+sub _json_bool {
+       return $_[0] ? JSON::true : JSON::false;
+}
+
 =head2 default
 
 Standard 404 error page