X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2Fstemmaweb%2FController%2FRoot.pm;h=7159680f297c41cfcb8f5740d3d88ade0f144a8f;hb=db0ac887720c37acee1e79201ae667ad0bd86401;hp=9d18c671a7e145c125134e1b5a0fbd9de8d29a5d;hpb=ce1c5863b59afa334ca4c61d1866e0710346f392;p=scpubgit%2Fstemmaweb.git diff --git a/lib/stemmaweb/Controller/Root.pm b/lib/stemmaweb/Controller/Root.pm index 9d18c67..7159680 100644 --- a/lib/stemmaweb/Controller/Root.pm +++ b/lib/stemmaweb/Controller/Root.pm @@ -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' } @@ -33,9 +35,35 @@ components will be loaded. sub index :Path :Args(0) { my ( $self, $c ) = @_; + # Are we being asked to load a text immediately? If so + if( $c->req->param('withtradition') ) { + $c->stash->{'withtradition'} = $c->req->param('withtradition'); + } $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 @@ -106,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; @@ -132,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 @@ -172,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; @@ -193,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: $@" ); } } @@ -219,19 +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'}; - unless( $tradition->has_user && $tradition->user->id eq $newownerid ) { + if( $tradition->has_user && !$tradition->user ) { + $tradition->clear_user; + } + unless( !$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; } @@ -250,13 +314,22 @@ 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 ], + # TODO Send them all with appropriate parameters so that the + # client side can choose what to display. + reltypes => [ map { $_->name } grep { !$_->is_weak && $_->is_colocation } + $tradition->collation->relationship_types ] }; - 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'); @@ -274,7 +347,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; @@ -283,6 +356,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 @@ -301,7 +390,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; @@ -314,15 +403,17 @@ sub stemma :Local :Args(2) { try { if( $stemmaid eq 'n' ) { # We are adding a new stemma. + $stemmaid = $tradition->stemma_count; $stemma = $tradition->add_stemma( 'dot' => $dot ); - $stemmaid = $tradition->stemma_count - 1; + } 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 ); @@ -340,7 +431,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') ) { @@ -356,11 +446,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'); } } @@ -378,19 +467,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: } + +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 #################### @@ -420,6 +581,10 @@ sub _json_error { return 0; } +sub _json_bool { + return $_[0] ? JSON::true : JSON::false; +} + =head2 default Standard 404 error page