query dynamically for relationship types in rel mapper
[scpubgit/stemmaweb.git] / lib / stemmaweb / Controller / Relation.pm
CommitLineData
b8a92065 1package stemmaweb::Controller::Relation;
2use Moose;
cc86fa11 3use Module::Load;
b8a92065 4use namespace::autoclean;
b28e606e 5use TryCatch;
b8a92065 6
7BEGIN { extends 'Catalyst::Controller' }
8
9
10=head1 NAME
11
12stemmaweb::Controller::Relation - Controller for the relationship mapper
13
14=head1 DESCRIPTION
15
b28e606e 16The reading relationship mapper with draggable nodes.
b8a92065 17
18=head1 METHODS
19
b28e606e 20=head2 index
21
b8a92065 22 GET relation/$textid
23
24Renders the application for the text identified by $textid.
25
b8a92065 26=cut
27
9529f69c 28sub index :Path :Args(0) {
29 my( $self, $c ) = @_;
b28e606e 30 $c->stash->{'template'} = 'relate.tt';
31}
32
9529f69c 33=head2 text
b28e606e 34
9529f69c 35 GET relation/$textid/
36
37 Runs the relationship mapper for the specified text ID.
38
b28e606e 39=cut
40
9529f69c 41sub text :Chained('/') :PathPart('relation') :CaptureArgs(1) {
42 my( $self, $c, $textid ) = @_;
13aa153c 43 my $tradition = $c->model('Directory')->tradition( $textid );
cd3f7f55 44 unless( $tradition ) {
45 $c->response->status('404');
46 $c->response->body("No such tradition with ID $textid");
47 $c->detach('View::Plain');
48 return;
49 }
50
7562a27b 51 # Account for a bad interaction between FastCGI and KiokuDB
52 unless( $tradition->collation->tradition ) {
53 $c->log->warn( "Fixing broken tradition link" );
54 $tradition->collation->_set_tradition( $tradition );
55 $c->model('Directory')->save( $tradition );
56 }
20198e59 57 # Check permissions. Will return 403 if denied, otherwise will
58 # put the appropriate value in the stash.
59 my $ok = _check_permission( $c, $tradition );
60 return unless $ok;
61
8843c8b9 62 $c->stash->{'textid'} = $textid;
63 $c->stash->{'tradition'} = $tradition;
64}
65
66sub main :Chained('text') :PathPart('') :Args(0) {
67 my( $self, $c ) = @_;
68 my $tradition = delete $c->stash->{'tradition'};
69 my $collation = $tradition->collation;
70
d58766c0 71 # See how big the tradition is. Edges are more important than nodes
72 # when it comes to rendering difficulty.
8843c8b9 73 my $numnodes = scalar $collation->readings;
74 my $numedges = scalar $collation->paths;
75 my $length = $collation->end->rank;
d58766c0 76 # We should display no more than roughly 500 nodes, or roughly 700
77 # edges, at a time.
78 my $segments = $numnodes / 500;
79 if( $numedges / 700 > $segments ) {
80 $segments = $numedges / 700;
81 }
82 my $segsize = sprintf( "%.0f", $length / $segments );
83 my $margin = sprintf( "%.0f", $segsize / 10 );
84 if( $segments > 1 ) {
13aa153c 85 # Segment the tradition in order not to overload the browser.
13aa153c 86 my @divs;
87 my $r = 0;
d58766c0 88 while( $r + $margin < $length ) {
13aa153c 89 push( @divs, $r );
d58766c0 90 $r += $segsize;
13aa153c 91 }
92 $c->stash->{'textsegments'} = [];
ea8e8b3c 93 foreach my $i ( 0..$#divs ) {
94 my $seg = { 'start' => $divs[$i] };
95 $seg->{'display'} = "Segment " . ($i+1);
13aa153c 96 push( @{$c->stash->{'textsegments'}}, $seg );
97 }
98 }
13aa153c 99 my $startseg = $c->req->param('start');
13aa153c 100 my $svgopts;
101 if( $startseg ) {
d58766c0 102 # Only render the subgraph from startseg to endseg or to END,
13aa153c 103 # whichever is less.
8843c8b9 104 my $endseg = $startseg + $segsize + $margin;
13aa153c 105 $svgopts = { 'from' => $startseg };
d58766c0 106 $svgopts->{'to'} = $endseg if $endseg < $collation->end->rank;
13aa153c 107 } elsif( exists $c->stash->{'textsegments'} ) {
108 # This is the unqualified load of a long tradition. We implicitly start
8843c8b9 109 # at zero, but go only as far as our segment size.
110 my $endseg = $segsize + $margin;
ea8e8b3c 111 $startseg = 0;
d58766c0 112 $svgopts = { 'to' => $endseg };
13aa153c 113 }
8843c8b9 114 # Spit out the SVG
13aa153c 115 my $svg_str = $collation->as_svg( $svgopts );
9529f69c 116 $svg_str =~ s/\n//gs;
ea8e8b3c 117 $c->stash->{'startseg'} = $startseg if defined $startseg;
9529f69c 118 $c->stash->{'svg_string'} = $svg_str;
119 $c->stash->{'text_title'} = $tradition->name;
487674b9 120 if( $tradition->can('language') ) {
121 $c->stash->{'text_lang'} = $tradition->language;
122 $c->stash->{'can_morphologize'} = 1;
123 } else {
124 $c->stash->{'text_lang'} = 'Default';
125 }
9529f69c 126 $c->stash->{'template'} = 'relate.tt';
b28e606e 127}
128
8843c8b9 129=head2 definitions
130
131 GET relation/$textid/definitions
132
133Returns a data structure giving the valid types and scopes for a relationship in
134this tradition.
135
136=cut
137
138sub definitions :Chained('text') :PathPart :Args(0) {
139 my( $self, $c ) = @_;
140 my $tradition = delete $c->stash->{'tradition'};
141 my @valid_relationships = map { $_->name } grep { !$_->is_weak }
142 $tradition->collation->relations->types;
143 my $valid_scopes = [ qw/ local global / ];
144 $c->stash->{'result'} = {
145 'types' => \@valid_relationships,
146 'scopes' => $valid_scopes
147 };
148 $c->forward('View::JSON');
149}
150
cc86fa11 151=head2 help
152
153 GET relation/help/$language
154
155Returns the help window HTML.
156
157=cut
158
159sub help :Local :Args(1) {
160 my( $self, $c, $lang ) = @_;
161 # Display the morphological help for the language if it is defined.
162 if( $lang && $lang ne 'Default' ) {
163 my $mod = 'Text::Tradition::Language::' . $lang;
164 try {
165 load( $mod );
166 } catch {
167 $c->log->debug("Warning: could not load $mod");
168 }
169 my $has_mod = $mod->can('morphology_tags');
cc86fa11 170 if( $has_mod ) {
171 my $tagset = &$has_mod;
172 $c->stash->{'tagset'} = $tagset;
173 }
174 }
175 $c->stash->{'template'} = 'relatehelp.tt';
176}
177
b28e606e 178=head2 relationships
179
13aa153c 180 GET relation/$textid/relationships
9529f69c 181
182Returns the list of relationships defined for this text.
b28e606e 183
13aa153c 184 POST relation/$textid/relationships { request }
9529f69c 185
186Attempts to define the requested relationship within the text. Returns 200 on
187success or 403 on error.
b28e606e 188
13aa153c 189 DELETE relation/$textid/relationships { request }
9529f69c 190
b28e606e 191
192=cut
193
9529f69c 194sub relationships :Chained('text') :PathPart :Args(0) {
b28e606e 195 my( $self, $c ) = @_;
6d124a83 196 my $tradition = delete $c->stash->{'tradition'};
20198e59 197 my $ok = _check_permission( $c, $tradition );
198 return unless $ok;
6d124a83 199 my $collation = $tradition->collation;
cdd592f3 200 my $m = $c->model('Directory');
9529f69c 201 if( $c->request->method eq 'GET' ) {
202 my @pairs = $collation->relationships; # returns the edges
203 my @all_relations;
204 foreach my $p ( @pairs ) {
205 my $relobj = $collation->relations->get_relationship( @$p );
545163a2 206 next if $relobj->type eq 'collated'; # Don't show these
7562a27b 207 next if $p->[0] eq $p->[1]; # HACK until bugfix
69a19c91 208 my $relhash = { source => $p->[0], target => $p->[1],
209 type => $relobj->type, scope => $relobj->scope };
210 $relhash->{'note'} = $relobj->annotation if $relobj->has_annotation;
211 push( @all_relations, $relhash );
9529f69c 212 }
213 $c->stash->{'result'} = \@all_relations;
20198e59 214 } else {
215 # Check write permissions first of all
216 if( $c->stash->{'permission'} ne 'full' ) {
9529f69c 217 $c->response->status( '403' );
20198e59 218 $c->stash->{'result'} = {
219 'error' => 'You do not have permission to view this tradition.' };
220 } elsif( $c->request->method eq 'POST' ) {
221 unless( $c->stash->{'permission'} eq 'full' ) {
222 $c->response->status( '403' );
223 $c->stash->{'result'} = {
224 'error' => 'You do not have permission to view this tradition.' };
225 $c->detach( 'View::JSON' );
226 }
227 my $node = $c->request->param('source_id');
228 my $target = $c->request->param('target_id');
229 my $relation = $c->request->param('rel_type');
230 my $note = $c->request->param('note');
231 my $scope = $c->request->param('scope');
232
8073db65 233 my $opts = { 'type' => $relation, 'propagate' => 1 };
234 $opts->{'scope'} = $scope if $scope;
20198e59 235 $opts->{'annotation'} = $note if $note;
236
237 try {
238 my @vectors = $collation->add_relationship( $node, $target, $opts );
239 $c->stash->{'result'} = \@vectors;
240 $m->save( $tradition );
241 } catch( Text::Tradition::Error $e ) {
242 $c->response->status( '403' );
243 $c->stash->{'result'} = { 'error' => $e->message };
244 }
245 } elsif( $c->request->method eq 'DELETE' ) {
246 my $node = $c->request->param('source_id');
247 my $target = $c->request->param('target_id');
248
249 try {
250 my @vectors = $collation->del_relationship( $node, $target );
251 $m->save( $tradition );
252 $c->stash->{'result'} = \@vectors;
253 } catch( Text::Tradition::Error $e ) {
254 $c->response->status( '403' );
255 $c->stash->{'result'} = { 'error' => $e->message };
256 }
9529f69c 257 }
b28e606e 258 }
b28e606e 259 $c->forward('View::JSON');
5f15640c 260}
261
262=head2 readings
263
264 GET relation/$textid/readings
265
266Returns the list of readings defined for this text along with their metadata.
267
268=cut
269
0dcdd5ec 270my %read_write_keys = (
271 'id' => 0,
272 'text' => 0,
273 'is_meta' => 0,
274 'grammar_invalid' => 1,
275 'is_nonsense' => 1,
276 'normal_form' => 1,
277);
278
5f15640c 279sub _reading_struct {
280 my( $reading ) = @_;
281 # Return a JSONable struct of the useful keys. Keys meant to be writable
282 # have a true value; read-only keys have a false value.
5f15640c 283 my $struct = {};
2be76d3f 284 map { $struct->{$_} = $reading->$_ if $reading->can( $_ ) } keys( %read_write_keys );
5f15640c 285 # Special case
2be76d3f 286 $struct->{'lexemes'} = $reading->can( 'lexemes' ) ? [ $reading->lexemes ] : [];
0dcdd5ec 287 # Look up any words related via spelling or orthography
288 my $sameword = sub {
289 my $t = $_[0]->type;
290 return $t eq 'spelling' || $t eq 'orthographic';
291 };
292 my @variants;
293 foreach my $sr ( $reading->related_readings( $sameword ) ) {
294 push( @variants, $sr->text );
295 }
296 $struct->{'variants'} = \@variants;
5f15640c 297 return $struct;
298}
299
300sub readings :Chained('text') :PathPart :Args(0) {
301 my( $self, $c ) = @_;
302 my $tradition = delete $c->stash->{'tradition'};
20198e59 303 my $ok = _check_permission( $c, $tradition );
304 return unless $ok;
5f15640c 305 my $collation = $tradition->collation;
306 my $m = $c->model('Directory');
307 if( $c->request->method eq 'GET' ) {
308 my $rdginfo = {};
309 foreach my $rdg ( $collation->readings ) {
310 $rdginfo->{$rdg->id} = _reading_struct( $rdg );
311 }
312 $c->stash->{'result'} = $rdginfo;
313 }
314 $c->forward('View::JSON');
315}
316
317=head2 reading
318
319 GET relation/$textid/reading/$id
320
321Returns the list of readings defined for this text along with their metadata.
322
323 POST relation/$textid/reading/$id { request }
324
325Alters the reading according to the values in request. Returns 403 Forbidden if
326the alteration isn't allowed.
327
328=cut
329
330sub reading :Chained('text') :PathPart :Args(1) {
331 my( $self, $c, $reading_id ) = @_;
332 my $tradition = delete $c->stash->{'tradition'};
333 my $collation = $tradition->collation;
0dcdd5ec 334 my $rdg = $collation->reading( $reading_id );
5f15640c 335 my $m = $c->model('Directory');
336 if( $c->request->method eq 'GET' ) {
5f15640c 337 $c->stash->{'result'} = $rdg ? _reading_struct( $rdg )
338 : { 'error' => "No reading with ID $reading_id" };
339 } elsif ( $c->request->method eq 'POST' ) {
20198e59 340 if( $c->stash->{'permission'} ne 'full' ) {
341 $c->response->status( '403' );
342 $c->stash->{'result'} = {
343 'error' => 'You do not have permission to view this tradition.' };
344 $c->detach('View::JSON');
487674b9 345 return;
20198e59 346 }
6666d111 347 my $errmsg;
487674b9 348 if( $rdg && $rdg->does('Text::Tradition::Morphology') ) {
349 # Are we re-lemmatizing?
350 if( $c->request->param('relemmatize') ) {
351 my $nf = $c->request->param('normal_form');
352 # TODO throw error unless $nf
353 $rdg->normal_form( $nf );
354 # TODO throw error if lemmatization fails
355 # TODO skip this if normal form hasn't changed
356 $rdg->lemmatize();
357 } else {
358 # Set all the values that we have for the reading.
359 # TODO error handling
360 foreach my $p ( keys %{$c->request->params} ) {
361 if( $p =~ /^morphology_(\d+)$/ ) {
362 # Set the form on the correct lexeme
363 my $morphval = $c->request->param( $p );
364 next unless $morphval;
365 my $midx = $1;
366 my $lx = $rdg->lexeme( $midx );
367 my $strrep = $rdg->language . ' // ' . $morphval;
368 my $idx = $lx->has_form( $strrep );
369 unless( defined $idx ) {
370 # Make the word form and add it to the lexeme.
371 try {
372 $idx = $lx->add_matching_form( $strrep ) - 1;
373 } catch( Text::Tradition::Error $e ) {
374 $c->response->status( '403' );
375 $errmsg = $e->message;
376 } catch {
377 # Something else went wrong, probably a Moose error
378 $c->response->status( '403' );
379 $errmsg = 'Something went wrong with the request';
380 }
6666d111 381 }
487674b9 382 $lx->disambiguate( $idx ) if defined $idx;
383 } elsif( $read_write_keys{$p} ) {
384 my $val = _clean_booleans( $rdg, $p, $c->request->param( $p ) );
385 $rdg->$p( $val );
0dcdd5ec 386 }
487674b9 387 }
388 }
389 $m->save( $rdg );
390 } else {
391 $errmsg = "Reading does not exist or cannot be morphologized";
0dcdd5ec 392 }
6666d111 393 $c->stash->{'result'} = $errmsg ? { 'error' => $errmsg }
394 : _reading_struct( $rdg );
0dcdd5ec 395
5f15640c 396 }
397 $c->forward('View::JSON');
398
399}
b28e606e 400
20198e59 401sub _check_permission {
402 my( $c, $tradition ) = @_;
403 my $user = $c->user_exists ? $c->user->get_object : undef;
b0524272 404 # Does this user have access?
20198e59 405 if( $user ) {
b0524272 406 if( $user->is_admin ||
407 ( $tradition->has_user && $tradition->user->id eq $user->id ) ) {
408 $c->stash->{'permission'} = 'full';
409 return 1;
410 }
080f8a02 411 }
412 # Is it public?
413 if( $tradition->public ) {
20198e59 414 $c->stash->{'permission'} = 'readonly';
415 return 1;
080f8a02 416 }
417 # Forbidden!
418 $c->response->status( 403 );
419 $c->response->body( 'You do not have permission to view this tradition.' );
420 $c->detach( 'View::Plain' );
421 return 0;
20198e59 422}
423
997ebe92 424sub _clean_booleans {
425 my( $rdg, $param, $val ) = @_;
426 if( $rdg->meta->get_attribute( $param )->type_constraint->name eq 'Bool' ) {
427 $val = 1 if $val eq 'true';
428 $val = undef if $val eq 'false';
429 }
430 return $val;
431}
432
b8a92065 433=head2 end
434
435Attempt to render a view, if needed.
436
437=cut
438
439sub end : ActionClass('RenderView') {}
440
441=head1 AUTHOR
442
443Tara L Andrews
444
445=head1 LICENSE
446
447This library is free software. You can redistribute it and/or modify
448it under the same terms as Perl itself.
449
450=cut
451
452__PACKAGE__->meta->make_immutable;
453
4541;