handle Excel 2007+ parsing as well
[scpubgit/stemmatology.git] / stemmaweb / lib / stemmaweb / Controller / Root.pm
CommitLineData
5c9ecf66 1package stemmaweb::Controller::Root;
dbcf12a6 2use Moose;
3use namespace::autoclean;
3837c155 4use Text::Tradition::Analysis qw/ run_analysis /;
852190b9 5use TryCatch;
3837c155 6
dbcf12a6 7
8BEGIN { extends 'Catalyst::Controller' }
9
10#
11# Sets the actions in this controller to be registered with no prefix
12# so they function identically to actions created in MyApp.pm
13#
14__PACKAGE__->config(namespace => '');
15
16=head1 NAME
17
5c9ecf66 18stemmaweb::Controller::Root - Root Controller for stemmaweb
dbcf12a6 19
20=head1 DESCRIPTION
21
6b70c348 22Serves up the main container pages.
dbcf12a6 23
6b70c348 24=head1 URLs
dbcf12a6 25
26=head2 index
27
6b70c348 28The root page (/). Serves the main container page, from which the various
29components will be loaded.
dbcf12a6 30
31=cut
32
33sub index :Path :Args(0) {
34 my ( $self, $c ) = @_;
35
6b70c348 36 $c->stash->{template} = 'index.tt';
37}
38
39=head1 Elements of index page
40
41=head2 directory
42
43 GET /directory
44
7c256818 45Serves a snippet of HTML that lists the available texts. This returns texts belonging to the logged-in user if any, otherwise it returns all public texts.
6b70c348 46
47=cut
7c256818 48
2376359f 49sub directory :Local :Args(0) {
6b70c348 50 my( $self, $c ) = @_;
3837c155 51 my $m = $c->model('Directory');
27a20fbe 52 # Is someone logged in?
3524c08f 53 my %usertexts;
27a20fbe 54 if( $c->user_exists ) {
55 my $user = $c->user->get_object;
3524c08f 56 my @list = $m->traditionlist( $user );
57 map { $usertexts{$_->{id}} = 1 } @list;
58 $c->stash->{usertexts} = \@list;
27a20fbe 59 $c->stash->{is_admin} = 1 if $user->is_admin;
60 }
3524c08f 61 # List public (i.e. readonly) texts separately from any user (i.e.
62 # full access) texts that exist. Admin users therefore have nothing
63 # in this list.
64 my @plist = grep { !$usertexts{$_->{id}} } $m->traditionlist('public');
65 $c->stash->{publictexts} = \@plist;
6b70c348 66 $c->stash->{template} = 'directory.tt';
67}
68
cf9626aa 69=head2 variantgraph
70
71 GET /variantgraph/$textid
72
73Returns the variant graph for the text specified at $textid, in SVG form.
74
75=cut
76
77sub variantgraph :Local :Args(1) {
78 my( $self, $c, $textid ) = @_;
3524c08f 79 my $tradition = $c->model('Directory')->tradition( $textid );
852190b9 80 my $ok = _check_permission( $c, $tradition );
81 return unless $ok;
82
b365fbae 83 my $collation = $tradition->collation;
cf9626aa 84 $c->stash->{'result'} = $collation->as_svg;
85 $c->forward('View::SVG');
86}
87
6b70c348 88=head2 alignment
89
90 GET /alignment/$textid
91
92Returns an alignment table for the text specified at $textid.
93
94=cut
95
2376359f 96sub alignment :Local :Args(1) {
6b70c348 97 my( $self, $c, $textid ) = @_;
3524c08f 98 my $tradition = $c->model('Directory')->tradition( $textid );
852190b9 99 my $ok = _check_permission( $c, $tradition );
100 return unless $ok;
101
3524c08f 102 my $collation = $tradition->collation;
7f52eac8 103 my $alignment = $collation->alignment_table;
6b70c348 104
105 # Turn the table, so that witnesses are by column and the rows
106 # are by rank.
107 my $wits = [ map { $_->{'witness'} } @{$alignment->{'alignment'}} ];
108 my $rows;
109 foreach my $i ( 0 .. $alignment->{'length'} - 1 ) {
110 my @rankrdgs = map { $_->{'tokens'}->[$i]->{'t'} }
111 @{$alignment->{'alignment'}};
112 push( @$rows, { 'rank' => $i+1, 'readings' => \@rankrdgs } );
113 }
6b70c348 114 $c->stash->{'witnesses'} = $wits;
115 $c->stash->{'table'} = $rows;
116 $c->stash->{'template'} = 'alignment.tt';
117}
118
119=head2 stemma
120
852190b9 121 GET /stemma/$textid/$stemmaid
6b70c348 122 POST /stemma/$textid, { 'dot' => $dot_string }
123
124Returns an SVG representation of the stemma hypothesis for the text. If
125the URL is called with POST and a new dot string, updates the stemma and
126returns the SVG as with GET.
127
128=cut
129
852190b9 130sub stemma :Local :Args {
131 my( $self, $c, $textid, $stemmaid ) = @_;
6b70c348 132 my $m = $c->model('Directory');
133 my $tradition = $m->tradition( $textid );
852190b9 134 my $ok = _check_permission( $c, $tradition );
135 return unless $ok;
136
137 $stemmaid = 0 unless defined $stemmaid;
138 $c->stash->{'result'} = '';
139 if( $tradition ) {
140 if( $c->req->method eq 'POST' ) {
141 # Update the stemma
142 my $dot = $c->request->body_params->{'dot'};
143 $tradition->add_stemma( $dot );
144 $m->store( $tradition );
145 $stemmaid = scalar( $tradition->stemma_count ) - 1;
146 }
147
148 $c->stash->{'result'} = $tradition->stemma_count > $stemmaid
149 ? $tradition->stemma( $stemmaid )->as_svg( { size => [ 500, 375 ] } )
150 : '';
6b70c348 151 }
6b70c348 152 $c->forward('View::SVG');
dbcf12a6 153}
154
6b70c348 155=head2 stemmadot
12720144 156
6b70c348 157 GET /stemmadot/$textid
158
159Returns the 'dot' format representation of the current stemma hypothesis.
160
161=cut
162
2376359f 163sub stemmadot :Local :Args(1) {
6b70c348 164 my( $self, $c, $textid ) = @_;
165 my $m = $c->model('Directory');
166 my $tradition = $m->tradition( $textid );
852190b9 167 my $ok = _check_permission( $c, $tradition );
168 return unless $ok;
6b70c348 169
170 $c->response->body( $tradition->stemma->editable );
171 $c->forward('View::Plain');
172}
12720144 173
852190b9 174=head1 AJAX methods for index page
175
176=head2 textinfo
177
178 GET /textinfo/$textid
179
180Returns information about a particular text.
181
182=cut
183
184sub textinfo :Local :Args(1) {
185 my( $self, $c, $textid ) = @_;
186 my $tradition = $c->model('Directory')->tradition( $textid );
187 my $ok = _check_permission( $c, $tradition );
188 return unless $ok;
189
190 # Need text name, witness list, scalar readings, scalar relationships, stemmata
191 my $textinfo = {
192 textid => $textid,
193 traditionname => $tradition->name,
194 witnesses => [ map { $_->sigil } $tradition->witnesses ],
195 readings => scalar $tradition->collation->readings,
196 relationships => scalar $tradition->collation->relationships
197 };
198 my @stemmasvg = map { $_->as_svg({ size => [ 500, 375 ] }) } $tradition->stemmata;
199 map { $_ =~ s/\n/ /mg } @stemmasvg;
200 $textinfo->{stemmata} = \@stemmasvg;
201 $c->stash->{'result'} = $textinfo;
202 $c->forward('View::JSON');
203}
204
205# TODO alter text parameters
206
207=head2 new
208
209 POST /newtradition { name: <name>, inputfile: <fileupload> }
210
211Creates a new tradition belonging to the logged-in user, according to the detected
212file type. Returns the ID and name of the new tradition.
213
214=cut
215
216sub newtradition :Local :Args(0) {
217 my( $self, $c ) = @_;
218 if( $c->user_exists ) {
219 my $user = $c->user->get_object;
220 # Grab the file upload, check its name/extension, and call the
221 # appropriate parser(s).
2b4baccc 222 my $upload = $c->request->upload('file');
852190b9 223 my $name = $c->request->param('name') || 'Uploaded tradition';
224 my( $ext ) = $upload->filename =~ /\.(\w+)$/;
225 my %newopts = (
226 'name' => $name,
2b4baccc 227 'file' => $upload->tempname
852190b9 228 );
229 my $tradition;
230 my $errmsg;
231 if( $ext eq 'xml' ) {
232 # Try the different XML parsing options to see if one works.
233 foreach my $type ( qw/ CollateX CTE TEI / ) {
234 try {
235 $tradition = Text::Tradition->new( %newopts, 'input' => $type );
236 } catch ( Text::Tradition::Error $e ) {
237 $errmsg = $e->message;
238 } catch {
239 $errmsg = "Unexpected parsing error";
240 }
241 last if $tradition;
242 }
3a3b8213 243 } elsif( $ext =~ /^(txt|csv|xls(x)?)$/ ) {
244 # If it's Excel we need to pass excel => $ext;
4b2a244a 245 # otherwise we need to pass sep_char => [record separator].
3a3b8213 246 if( $ext =~ /xls/ ) {
247 $newopts{'excel'} = $ext;
248 } else {
249 $newopts{'sep_char'} = $ext eq 'txt' ? "\t" : ',';
250 }
852190b9 251 try {
252 $tradition = Text::Tradition->new(
253 %newopts,
254 'input' => 'Tabular',
852190b9 255 );
256 } catch ( Text::Tradition::Error $e ) {
257 $errmsg = $e->message;
258 } catch {
259 $errmsg = "Unexpected parsing error";
260 }
4b2a244a 261 } elsif( $ext eq 'xlsx' ) {
852190b9 262 $c->stash->{'result'} =
4b2a244a 263 { 'error' => "Excel XML parsing not supported yet" };
852190b9 264 $c->response->status( 500 );
265 } else {
266 # Error unless we have a recognized filename extension
267 $c->stash->{'result'} =
268 { 'error' => "Unrecognized file type extension $ext" };
269 $c->response->status( 500 );
270 }
271
272 # Save the tradition if we have it, and return its data or else the
273 # error that occurred trying to make it.
274 if( $tradition ) {
2b4baccc 275 my $m = $c->model('Directory');
276 $user->add_tradition( $tradition );
852190b9 277 my $id = $c->model('Directory')->store( $tradition );
2b4baccc 278 $c->model('Directory')->store( $user );
852190b9 279 $c->stash->{'result'} = { 'id' => $id, 'name' => $tradition->name };
280 } else {
281 $c->stash->{'result'} =
282 { 'error' => "Error parsing tradition .$ext file: $errmsg" };
283 $c->response->status( 500 );
284 }
285 } else {
286 $c->stash->{'result'} =
287 { 'error' => 'Cannot save a tradition without being logged in' };
288 $c->response->status( 403 );
289 }
290 $c->forward('View::JSON');
291}
292
293sub _check_permission {
294 my( $c, $tradition ) = @_;
295 my $user = $c->user_exists ? $c->user->get_object : undef;
296 if( $user ) {
2b4baccc 297 return 'full' if ( $user->is_admin ||
298 ( $tradition->has_user && $tradition->user->id eq $user->id ) );
75ce4f7a 299 }
300 # Text doesn't belong to us, so maybe it's public?
301 return 'readonly' if $tradition->public;
302
303 # ...nope. Forbidden!
2b4baccc 304 $c->response->status( 403 );
305 $c->response->body( 'You do not have permission to view this tradition.' );
306 $c->detach( 'View::Plain' );
307 return 0;
852190b9 308}
309
dbcf12a6 310=head2 default
311
312Standard 404 error page
313
314=cut
315
316sub default :Path {
317 my ( $self, $c ) = @_;
318 $c->response->body( 'Page not found' );
319 $c->response->status(404);
320}
321
322=head2 end
323
324Attempt to render a view, if needed.
325
326=cut
327
328sub end : ActionClass('RenderView') {}
329
330=head1 AUTHOR
331
332Tara L Andrews
333
334=head1 LICENSE
335
336This library is free software. You can redistribute it and/or modify
337it under the same terms as Perl itself.
338
339=cut
340
341__PACKAGE__->meta->make_immutable;
342
3431;