Hide error when merging a new item
[scpubgit/stemmaweb.git] / lib / stemmaweb / Controller / Relation.pm
CommitLineData
b8a92065 1package stemmaweb::Controller::Relation;
5539cba3 2use JSON qw/ to_json from_json /;
b8a92065 3use Moose;
e4bdf660 4use Moose::Util::TypeConstraints qw/ find_type_constraint /;
cc86fa11 5use Module::Load;
b8a92065 6use namespace::autoclean;
e4bdf660 7use Text::Tradition::Datatypes;
b28e606e 8use TryCatch;
b8a92065 9
10BEGIN { extends 'Catalyst::Controller' }
11
b8a92065 12=head1 NAME
13
14stemmaweb::Controller::Relation - Controller for the relationship mapper
15
16=head1 DESCRIPTION
17
b28e606e 18The reading relationship mapper with draggable nodes.
b8a92065 19
20=head1 METHODS
21
b28e606e 22=head2 index
23
b8a92065 24 GET relation/$textid
25
26Renders the application for the text identified by $textid.
27
b8a92065 28=cut
29
9529f69c 30sub index :Path :Args(0) {
31 my( $self, $c ) = @_;
b28e606e 32 $c->stash->{'template'} = 'relate.tt';
33}
34
9529f69c 35=head2 text
b28e606e 36
9529f69c 37 GET relation/$textid/
38
39 Runs the relationship mapper for the specified text ID.
40
b28e606e 41=cut
42
9529f69c 43sub text :Chained('/') :PathPart('relation') :CaptureArgs(1) {
44 my( $self, $c, $textid ) = @_;
13aa153c 45 my $tradition = $c->model('Directory')->tradition( $textid );
cd3f7f55 46 unless( $tradition ) {
47 $c->response->status('404');
48 $c->response->body("No such tradition with ID $textid");
49 $c->detach('View::Plain');
50 return;
51 }
52
7562a27b 53 # Account for a bad interaction between FastCGI and KiokuDB
54 unless( $tradition->collation->tradition ) {
55 $c->log->warn( "Fixing broken tradition link" );
56 $tradition->collation->_set_tradition( $tradition );
57 $c->model('Directory')->save( $tradition );
58 }
20198e59 59 # Check permissions. Will return 403 if denied, otherwise will
60 # put the appropriate value in the stash.
61 my $ok = _check_permission( $c, $tradition );
62 return unless $ok;
63
8843c8b9 64 $c->stash->{'textid'} = $textid;
65 $c->stash->{'tradition'} = $tradition;
66}
67
68sub main :Chained('text') :PathPart('') :Args(0) {
69 my( $self, $c ) = @_;
70 my $tradition = delete $c->stash->{'tradition'};
71 my $collation = $tradition->collation;
89aae3ee 72
73 # Stash text direction to use in JS.
74 $c->stash->{'direction'} = $collation->direction;
75
56e3972e 76 # Stash the relationship definitions
e4bdf660 77 $c->stash->{'relationship_scopes'} =
78 to_json( find_type_constraint( 'RelationshipScope' )->values );
79 $c->stash->{'ternary_values'} =
80 to_json( find_type_constraint( 'Ternary' )->values );
56e3972e 81 my @reltypeinfo;
82 foreach my $type ( sort { _typesort( $a, $b ) } $collation->relations->types ) {
83 next if $type->is_weak;
84 my $struct = { name => $type->name, description => $type->description };
85 push( @reltypeinfo, $struct );
86 }
87 $c->stash->{'relationship_types'} = to_json( \@reltypeinfo );
88
d58766c0 89 # See how big the tradition is. Edges are more important than nodes
90 # when it comes to rendering difficulty.
8843c8b9 91 my $numnodes = scalar $collation->readings;
92 my $numedges = scalar $collation->paths;
93 my $length = $collation->end->rank;
d58766c0 94 # We should display no more than roughly 500 nodes, or roughly 700
95 # edges, at a time.
96 my $segments = $numnodes / 500;
97 if( $numedges / 700 > $segments ) {
98 $segments = $numedges / 700;
99 }
100 my $segsize = sprintf( "%.0f", $length / $segments );
101 my $margin = sprintf( "%.0f", $segsize / 10 );
102 if( $segments > 1 ) {
13aa153c 103 # Segment the tradition in order not to overload the browser.
13aa153c 104 my @divs;
105 my $r = 0;
d58766c0 106 while( $r + $margin < $length ) {
13aa153c 107 push( @divs, $r );
d58766c0 108 $r += $segsize;
13aa153c 109 }
110 $c->stash->{'textsegments'} = [];
ea8e8b3c 111 foreach my $i ( 0..$#divs ) {
112 my $seg = { 'start' => $divs[$i] };
113 $seg->{'display'} = "Segment " . ($i+1);
13aa153c 114 push( @{$c->stash->{'textsegments'}}, $seg );
115 }
116 }
13aa153c 117 my $startseg = $c->req->param('start');
13aa153c 118 my $svgopts;
119 if( $startseg ) {
d58766c0 120 # Only render the subgraph from startseg to endseg or to END,
13aa153c 121 # whichever is less.
8843c8b9 122 my $endseg = $startseg + $segsize + $margin;
13aa153c 123 $svgopts = { 'from' => $startseg };
d58766c0 124 $svgopts->{'to'} = $endseg if $endseg < $collation->end->rank;
13aa153c 125 } elsif( exists $c->stash->{'textsegments'} ) {
126 # This is the unqualified load of a long tradition. We implicitly start
8843c8b9 127 # at zero, but go only as far as our segment size.
128 my $endseg = $segsize + $margin;
ea8e8b3c 129 $startseg = 0;
d58766c0 130 $svgopts = { 'to' => $endseg };
13aa153c 131 }
8843c8b9 132 # Spit out the SVG
13aa153c 133 my $svg_str = $collation->as_svg( $svgopts );
9529f69c 134 $svg_str =~ s/\n//gs;
ea8e8b3c 135 $c->stash->{'startseg'} = $startseg if defined $startseg;
9529f69c 136 $c->stash->{'svg_string'} = $svg_str;
137 $c->stash->{'text_title'} = $tradition->name;
d935aef8 138 if( $tradition->can('language') && $tradition->language ) {
487674b9 139 $c->stash->{'text_lang'} = $tradition->language;
140 $c->stash->{'can_morphologize'} = 1;
141 } else {
142 $c->stash->{'text_lang'} = 'Default';
143 }
9529f69c 144 $c->stash->{'template'} = 'relate.tt';
b28e606e 145}
146
56e3972e 147sub _typesort {
148 my( $a, $b ) = @_;
149 my $blsort = $a->bindlevel <=> $b->bindlevel;
150 return $blsort if $blsort;
151 return $a->name cmp $b->name;
8843c8b9 152}
153
cc86fa11 154=head2 help
155
156 GET relation/help/$language
157
158Returns the help window HTML.
159
160=cut
161
162sub help :Local :Args(1) {
163 my( $self, $c, $lang ) = @_;
164 # Display the morphological help for the language if it is defined.
165 if( $lang && $lang ne 'Default' ) {
166 my $mod = 'Text::Tradition::Language::' . $lang;
167 try {
168 load( $mod );
169 } catch {
170 $c->log->debug("Warning: could not load $mod");
171 }
172 my $has_mod = $mod->can('morphology_tags');
cc86fa11 173 if( $has_mod ) {
174 my $tagset = &$has_mod;
175 $c->stash->{'tagset'} = $tagset;
176 }
177 }
178 $c->stash->{'template'} = 'relatehelp.tt';
179}
180
b28e606e 181=head2 relationships
182
13aa153c 183 GET relation/$textid/relationships
9529f69c 184
185Returns the list of relationships defined for this text.
b28e606e 186
13aa153c 187 POST relation/$textid/relationships { request }
9529f69c 188
189Attempts to define the requested relationship within the text. Returns 200 on
190success or 403 on error.
b28e606e 191
13aa153c 192 DELETE relation/$textid/relationships { request }
9529f69c 193
b28e606e 194
195=cut
196
9529f69c 197sub relationships :Chained('text') :PathPart :Args(0) {
b28e606e 198 my( $self, $c ) = @_;
6d124a83 199 my $tradition = delete $c->stash->{'tradition'};
20198e59 200 my $ok = _check_permission( $c, $tradition );
201 return unless $ok;
6d124a83 202 my $collation = $tradition->collation;
cdd592f3 203 my $m = $c->model('Directory');
9529f69c 204 if( $c->request->method eq 'GET' ) {
205 my @pairs = $collation->relationships; # returns the edges
206 my @all_relations;
207 foreach my $p ( @pairs ) {
208 my $relobj = $collation->relations->get_relationship( @$p );
545163a2 209 next if $relobj->type eq 'collated'; # Don't show these
7562a27b 210 next if $p->[0] eq $p->[1]; # HACK until bugfix
eefe56ac 211 my $relhash = { source_id => $p->[0], target_id => $p->[1],
212 source_text => $collation->reading( $p->[0] )->text,
213 target_text => $collation->reading( $p->[1] )->text,
214 type => $relobj->type, scope => $relobj->scope,
215 a_derivable_from_b => $relobj->a_derivable_from_b,
216 b_derivable_from_a => $relobj->b_derivable_from_a,
217 non_independent => $relobj->non_independent,
e4bdf660 218 is_significant => $relobj->is_significant
eefe56ac 219 };
69a19c91 220 $relhash->{'note'} = $relobj->annotation if $relobj->has_annotation;
221 push( @all_relations, $relhash );
9529f69c 222 }
223 $c->stash->{'result'} = \@all_relations;
20198e59 224 } else {
225 # Check write permissions first of all
226 if( $c->stash->{'permission'} ne 'full' ) {
9529f69c 227 $c->response->status( '403' );
20198e59 228 $c->stash->{'result'} = {
56e3972e 229 'error' => 'You do not have permission to modify this tradition.' };
230 $c->detach( 'View::JSON' );
20198e59 231 } elsif( $c->request->method eq 'POST' ) {
eefe56ac 232 my $opts = $c->request->params;
233
234 # Retrieve the source / target from the options
235 my $node = delete $opts->{source_id};
236 my $target = delete $opts->{target_id};
237
238 # Make sure we didn't send a blank or invalid relationship type
239 my $relation = $opts->{type};
240 unless( $collation->get_relationship_type( $relation ) ) {
241 my $errmsg = $relation ? "No such relationship type $relation" :
242 "You must specify a relationship type";
243 $c->stash->{'result'} = { error => $errmsg };
244 $c->response->status( '400' );
245 $c->detach( 'View::JSON' );
246 }
247
248 # Keep the data clean
249 my @booleans = qw/ a_derivable_from_b b_derivable_from_a non_independent /;
250 foreach my $k ( keys %$opts ) {
251 if( $opts->{$k} && grep { $_ eq $k } @booleans ) {
252 $opts->{$k} = 1;
253 }
254 }
20198e59 255
eefe56ac 256 delete $opts->{scope} unless $opts->{scope};
257 delete $opts->{annotation} unless $opts->{annotation};
995efe76 258 delete $opts->{is_significant} unless $opts->{is_significant};
eefe56ac 259 $opts->{propagate} = 1;
20198e59 260
261 try {
262 my @vectors = $collation->add_relationship( $node, $target, $opts );
263 $c->stash->{'result'} = \@vectors;
264 $m->save( $tradition );
265 } catch( Text::Tradition::Error $e ) {
266 $c->response->status( '403' );
995efe76 267 $c->stash->{'result'} = { error => $e->message };
268 } catch {
269 $c->response->status( '500' );
270 $c->stash->{'result'} = { error => "Something went wrong with the request" };
20198e59 271 }
272 } elsif( $c->request->method eq 'DELETE' ) {
273 my $node = $c->request->param('source_id');
274 my $target = $c->request->param('target_id');
088a14af 275 my $scopewide = $c->request->param('scopewide')
276 && $c->request->param('scopewide') eq 'true';
20198e59 277 try {
088a14af 278 my @vectors = $collation->del_relationship( $node, $target, $scopewide );
20198e59 279 $m->save( $tradition );
280 $c->stash->{'result'} = \@vectors;
281 } catch( Text::Tradition::Error $e ) {
282 $c->response->status( '403' );
283 $c->stash->{'result'} = { 'error' => $e->message };
995efe76 284 } catch {
285 $c->response->status( '500' );
286 $c->stash->{'result'} = { error => "Something went wrong with the request" };
287 }
9529f69c 288 }
b28e606e 289 }
b28e606e 290 $c->forward('View::JSON');
5f15640c 291}
292
293=head2 readings
294
295 GET relation/$textid/readings
296
297Returns the list of readings defined for this text along with their metadata.
298
299=cut
300
0dcdd5ec 301my %read_write_keys = (
302 'id' => 0,
303 'text' => 0,
304 'is_meta' => 0,
305 'grammar_invalid' => 1,
306 'is_nonsense' => 1,
307 'normal_form' => 1,
308);
309
5f15640c 310sub _reading_struct {
311 my( $reading ) = @_;
312 # Return a JSONable struct of the useful keys. Keys meant to be writable
313 # have a true value; read-only keys have a false value.
5f15640c 314 my $struct = {};
2be76d3f 315 map { $struct->{$_} = $reading->$_ if $reading->can( $_ ) } keys( %read_write_keys );
5f15640c 316 # Special case
2be76d3f 317 $struct->{'lexemes'} = $reading->can( 'lexemes' ) ? [ $reading->lexemes ] : [];
0dcdd5ec 318 # Look up any words related via spelling or orthography
319 my $sameword = sub {
320 my $t = $_[0]->type;
321 return $t eq 'spelling' || $t eq 'orthographic';
322 };
5539cba3 323 # Now add the list data
324 $struct->{'variants'} = [ map { $_->text } $reading->related_readings( $sameword ) ];
325 $struct->{'witnesses'} = [ $reading->witnesses ];
5f15640c 326 return $struct;
327}
328
329sub readings :Chained('text') :PathPart :Args(0) {
330 my( $self, $c ) = @_;
331 my $tradition = delete $c->stash->{'tradition'};
20198e59 332 my $ok = _check_permission( $c, $tradition );
333 return unless $ok;
5f15640c 334 my $collation = $tradition->collation;
335 my $m = $c->model('Directory');
336 if( $c->request->method eq 'GET' ) {
337 my $rdginfo = {};
338 foreach my $rdg ( $collation->readings ) {
339 $rdginfo->{$rdg->id} = _reading_struct( $rdg );
340 }
341 $c->stash->{'result'} = $rdginfo;
342 }
343 $c->forward('View::JSON');
344}
345
346=head2 reading
347
348 GET relation/$textid/reading/$id
349
350Returns the list of readings defined for this text along with their metadata.
351
352 POST relation/$textid/reading/$id { request }
353
354Alters the reading according to the values in request. Returns 403 Forbidden if
355the alteration isn't allowed.
356
357=cut
358
359sub reading :Chained('text') :PathPart :Args(1) {
360 my( $self, $c, $reading_id ) = @_;
361 my $tradition = delete $c->stash->{'tradition'};
362 my $collation = $tradition->collation;
0dcdd5ec 363 my $rdg = $collation->reading( $reading_id );
5f15640c 364 my $m = $c->model('Directory');
365 if( $c->request->method eq 'GET' ) {
5f15640c 366 $c->stash->{'result'} = $rdg ? _reading_struct( $rdg )
367 : { 'error' => "No reading with ID $reading_id" };
368 } elsif ( $c->request->method eq 'POST' ) {
20198e59 369 if( $c->stash->{'permission'} ne 'full' ) {
370 $c->response->status( '403' );
371 $c->stash->{'result'} = {
5539cba3 372 'error' => 'You do not have permission to modify this tradition.' };
20198e59 373 $c->detach('View::JSON');
487674b9 374 return;
20198e59 375 }
6666d111 376 my $errmsg;
487674b9 377 if( $rdg && $rdg->does('Text::Tradition::Morphology') ) {
378 # Are we re-lemmatizing?
379 if( $c->request->param('relemmatize') ) {
380 my $nf = $c->request->param('normal_form');
381 # TODO throw error unless $nf
382 $rdg->normal_form( $nf );
383 # TODO throw error if lemmatization fails
384 # TODO skip this if normal form hasn't changed
385 $rdg->lemmatize();
386 } else {
387 # Set all the values that we have for the reading.
388 # TODO error handling
389 foreach my $p ( keys %{$c->request->params} ) {
390 if( $p =~ /^morphology_(\d+)$/ ) {
391 # Set the form on the correct lexeme
392 my $morphval = $c->request->param( $p );
393 next unless $morphval;
394 my $midx = $1;
395 my $lx = $rdg->lexeme( $midx );
396 my $strrep = $rdg->language . ' // ' . $morphval;
397 my $idx = $lx->has_form( $strrep );
398 unless( defined $idx ) {
399 # Make the word form and add it to the lexeme.
400 try {
401 $idx = $lx->add_matching_form( $strrep ) - 1;
402 } catch( Text::Tradition::Error $e ) {
403 $c->response->status( '403' );
404 $errmsg = $e->message;
405 } catch {
406 # Something else went wrong, probably a Moose error
995efe76 407 $c->response->status( '500' );
487674b9 408 $errmsg = 'Something went wrong with the request';
409 }
6666d111 410 }
487674b9 411 $lx->disambiguate( $idx ) if defined $idx;
412 } elsif( $read_write_keys{$p} ) {
413 my $val = _clean_booleans( $rdg, $p, $c->request->param( $p ) );
414 $rdg->$p( $val );
0dcdd5ec 415 }
487674b9 416 }
417 }
418 $m->save( $rdg );
419 } else {
420 $errmsg = "Reading does not exist or cannot be morphologized";
0dcdd5ec 421 }
6666d111 422 $c->stash->{'result'} = $errmsg ? { 'error' => $errmsg }
423 : _reading_struct( $rdg );
0dcdd5ec 424
5f15640c 425 }
426 $c->forward('View::JSON');
427
428}
b28e606e 429
a51e34c5 430sub compress :Chained('text') :PathPart :Args(0) {
431 my( $self, $c ) = @_;
432 my $tradition = delete $c->stash->{'tradition'};
433 my $collation = $tradition->collation;
434 my $m = $c->model('Directory');
435
436 my @rids = $c->request->param('readings[]');
437 my @readings;
438
439 foreach my $rid (@rids) {
440 my $rdg = $collation->reading( $rid );
441
442 push @readings, $rdg;
443 }
444
445 my $len = scalar @readings;
446
447 if( $c->request->method eq 'POST' ) {
448 if( $c->stash->{'permission'} ne 'full' ) {
449 $c->response->status( '403' );
450 $c->stash->{'result'} = {
451 'error' => 'You do not have permission to modify this tradition.' };
452 $c->detach('View::JSON');
453 return;
454 }
455
456 # Sanity check: first save the original text of each witness.
457 my %origtext;
458 foreach my $wit ( $tradition->witnesses ) {
459 $origtext{$wit->sigil} = $collation->path_text( $wit->sigil );
460 if( $wit->is_layered ) {
461 my $acsig = $wit->sigil . $collation->ac_label;
462 $origtext{$acsig} = $collation->path_text( $acsig );
463 }
464 }
465
466 my $first = 0;
467
468 for (my $i = 0; $i < $len; $i++) {
469 my $rdg = $readings[$i];
470
471 if ($rdg->is_combinable) {
472 $first = $i;
473 last;
474 }
475 }
476
2d34519e 477 my @nodes;
478 push @nodes, "$readings[$first]";
479
a51e34c5 480 for (my $i = $first+1; $i < $len; $i++) {
481 my $rdg = $readings[$first];
482 my $next = $readings[$i];
483
484 last unless $next->is_combinable;
2d34519e 485 push @nodes, "$next";
a51e34c5 486
2d34519e 487 try {
488 $collation->merge_readings( "$rdg", "$next", 1 );
489 } catch ($e) {
490 $c->stash->{result} = {
491 error_msg => $e->message,
492 };
a51e34c5 493
2d34519e 494 $c->detach('View::JSON');
495 }
a51e34c5 496 }
497
2d34519e 498 try {
499 # Finally, make sure we haven't screwed anything up.
500 foreach my $wit ( $tradition->witnesses ) {
501 my $pathtext = $collation->path_text( $wit->sigil );
32f13891 502 Text::Tradition::Error->throw_collation_error( "Text differs for witness " . $wit->sigil )
2d34519e 503 unless $pathtext eq $origtext{$wit->sigil};
504 if( $wit->is_layered ) {
505 my $acsig = $wit->sigil . $collation->ac_label;
506 $pathtext = $collation->path_text( $acsig );
32f13891 507 Text::Tradition::Error->throw_collation_error( "Layered text differs for witness " . $wit->sigil )
2d34519e 508 unless $pathtext eq $origtext{$acsig};
509 }
a51e34c5 510 }
32f13891 511 } catch (Text::Tradition::Error $e) {
2d34519e 512 $c->stash->{result} = {
513 error_msg => $e->message,
514 };
515
516 $c->detach('View::JSON');
a51e34c5 517 }
518
2d34519e 519
a51e34c5 520 $collation->relations->rebuild_equivalence();
521 $collation->calculate_ranks();
522
523 $m->save($collation);
524
2a65f5c9 525 if ($collation->direction eq 'RL') {
526 @nodes = reverse @nodes;
527 }
2d34519e 528
529 $c->stash->{'result'} = {
530 success => 1,
531 nodes => \@nodes,
532 };
533
a51e34c5 534 $c->forward('View::JSON');
535 }
536}
537
b001c73d 538=head2 merge
539
540 POST relation/$textid/merge { data }
541
542Merges the requested readings, combining the witnesses of both readings into
543the target reading. All non-conflicting source relationships are inherited by
544the target relationship.
545
546=cut
547
548sub merge :Chained('text') :PathPart :Args(0) {
549 my( $self, $c ) = @_;
550 my $tradition = delete $c->stash->{'tradition'};
551 my $collation = $tradition->collation;
552 my $m = $c->model('Directory');
553 if( $c->request->method eq 'POST' ) {
554 if( $c->stash->{'permission'} ne 'full' ) {
555 $c->response->status( '403' );
556 $c->stash->{'result'} = {
557 'error' => 'You do not have permission to modify this tradition.' };
558 $c->detach('View::JSON');
559 return;
560 }
561 my $errmsg;
562 my $response;
563
564 my $main = $c->request->param('target_id');
565 my $second = $c->request->param('source_id');
566 # Find the common successor of these, so that we can detect other
567 # potentially identical readings.
568 my $csucc = $collation->common_successor( $main, $second );
569
570 # Try the merge if these are parallel readings.
571 if( $csucc->id eq $main || $csucc->id eq $second ) {
572 $errmsg = "Cannot merge readings in the same path";
573 } else {
574 try {
575 $collation->merge_readings( $main, $second );
576 } catch( Text::Tradition::Error $e ) {
577 $c->response->status( '403' );
578 $errmsg = $e->message;
579 } catch {
580 # Something else went wrong, probably a Moose error
581 $c->response->status( '403' );
582 $errmsg = 'Something went wrong with the request';
583 }
584 }
585
586 # Look for readings that are now identical.
587 if( $errmsg ) {
588 $response = { status => 'error', error => $errmsg };
589 } else {
590 $response = { status => 'ok' };
8880c19d 591 unless( $c->request->param('single') ) {
592 my @identical = $collation->identical_readings(
593 start => $main, end => $csucc->id );
594 if( @identical ) {
595 $response->{'checkalign'} = [
596 map { [ $_->[0]->id, $_->[1]->id ] } @identical ];
597 }
b001c73d 598 }
599 $m->save( $collation );
600 }
601 $c->stash->{'result'} = $response;
602 $c->forward('View::JSON');
603 }
604}
605
5539cba3 606=head2 duplicate
607
fdb37581 608 POST relation/$textid/duplicate { data }
5539cba3 609
fdb37581 610Duplicates the requested readings, detaching the witnesses specified in
611the list to use the new reading(s) instead of the old. The data to be
612passed should be a JSON structure:
613
614 { readings: rid1,rid2,rid3,...
615 witnesses: [ wit1, ... ] }
5539cba3 616
617=cut
618
fdb37581 619sub duplicate :Chained('text') :PathPart :Args(0) {
620 my( $self, $c ) = @_;
5539cba3 621 my $tradition = delete $c->stash->{'tradition'};
622 my $collation = $tradition->collation;
5539cba3 623 my $m = $c->model('Directory');
624 if( $c->request->method eq 'POST' ) {
625 if( $c->stash->{'permission'} ne 'full' ) {
626 $c->response->status( '403' );
627 $c->stash->{'result'} = {
628 'error' => 'You do not have permission to modify this tradition.' };
629 $c->detach('View::JSON');
630 return;
631 }
632 my $errmsg;
633 my $response = {};
fdb37581 634 # Sort out which readings need to be duplicated from the set given, and
635 # ensure that all the given wits bear each relevant reading.
636
637 my %wits = ();
638 map { $wits{$_} = 1 } $c->request->param('witnesses[]');
639 my %rdgranks = ();
640 foreach my $rid ( $c->request->param('readings[]') ) {
641 my $numwits = 0;
642 my $rdg = $collation->reading( $rid );
643 foreach my $rwit ( $rdg->witnesses( $rid ) ) {
644 $numwits++ if exists $wits{$rwit};
645 }
769401c3 646 next unless $numwits; # Disregard readings with none of our witnesses
647 if( $numwits < keys( %wits ) ) {
fdb37581 648 $errmsg = "Reading $rid contains some but not all of the specified witnesses.";
649 last;
650 } elsif( exists $rdgranks{ $rdg->rank } ) {
651 $errmsg = "More than one reading would be detached along with $rid at rank " . $rdg->rank;
652 last;
653 } else {
654 $rdgranks{ $rdg->rank } = $rid;
655 }
656 }
657
658 # Now check that the readings make a single sequence.
659 unless( $errmsg ) {
660 my $prior;
661 foreach my $rank ( sort { $a <=> $b } keys %rdgranks ) {
662 my $rid = $rdgranks{$rank};
663 if( $prior ) {
664 # Check that there is only one path between $prior and $rdg.
665 foreach my $wit ( keys %wits ) {
666 unless( $collation->prior_reading( $rid, $wit ) eq $prior ) {
667 $errmsg = "Diverging witness paths from $prior to $rid at $wit";
668 last;
669 }
670 }
671 }
672 $prior = $rid;
673 }
674 }
675
676 # Abort if we've run into a problem.
677 if( $errmsg ) {
678 $c->stash->{'result'} = { 'error' => $errmsg };
679 $c->response->status( '403' );
680 $c->forward('View::JSON');
681 return;
682 }
683
684 # Otherwise, do the dirty work.
685 my @witlist = keys %wits;
217f5e64 686 my @deleted_relations;
fdb37581 687 foreach my $rank ( sort { $a <=> $b } keys %rdgranks ) {
5539cba3 688 my $newrdg;
fdb37581 689 my $reading_id = $rdgranks{$rank};
217f5e64 690 my @delrels;
5539cba3 691 try {
217f5e64 692 ( $newrdg, @delrels ) =
693 $collation->duplicate_reading( $reading_id, @witlist );
5539cba3 694 } catch( Text::Tradition::Error $e ) {
695 $c->response->status( '403' );
696 $errmsg = $e->message;
697 } catch {
698 # Something else went wrong, probably a Moose error
995efe76 699 $c->response->status( '500' );
5539cba3 700 $errmsg = 'Something went wrong with the request';
701 }
702 if( $newrdg ) {
ea77ecb8 703 my $data = _reading_struct( $newrdg );
704 $data->{'orig_rdg'} = $reading_id;
705 $response->{"$newrdg"} = $data;
217f5e64 706 push( @deleted_relations, @delrels );
5539cba3 707 }
fdb37581 708 }
709 if( $errmsg ) {
710 $c->stash->{'result'} = { 'error' => $errmsg };
5539cba3 711 } else {
fdb37581 712 $m->save( $collation );
217f5e64 713 $response->{'DELETED'} = \@deleted_relations;
fdb37581 714 $c->stash->{'result'} = $response;
5539cba3 715 }
5539cba3 716 }
717 $c->forward('View::JSON');
718}
719
720
721
20198e59 722sub _check_permission {
723 my( $c, $tradition ) = @_;
724 my $user = $c->user_exists ? $c->user->get_object : undef;
b0524272 725 # Does this user have access?
20198e59 726 if( $user ) {
b0524272 727 if( $user->is_admin ||
728 ( $tradition->has_user && $tradition->user->id eq $user->id ) ) {
729 $c->stash->{'permission'} = 'full';
730 return 1;
731 }
080f8a02 732 }
733 # Is it public?
734 if( $tradition->public ) {
20198e59 735 $c->stash->{'permission'} = 'readonly';
736 return 1;
080f8a02 737 }
738 # Forbidden!
739 $c->response->status( 403 );
740 $c->response->body( 'You do not have permission to view this tradition.' );
741 $c->detach( 'View::Plain' );
742 return 0;
20198e59 743}
744
997ebe92 745sub _clean_booleans {
eefe56ac 746 my( $obj, $param, $val ) = @_;
747 if( $obj->meta->get_attribute( $param )->type_constraint->name eq 'Bool' ) {
997ebe92 748 $val = 1 if $val eq 'true';
749 $val = undef if $val eq 'false';
750 }
751 return $val;
752}
753
b8a92065 754=head2 end
755
756Attempt to render a view, if needed.
757
758=cut
759
760sub end : ActionClass('RenderView') {}
761
762=head1 AUTHOR
763
764Tara L Andrews
765
766=head1 LICENSE
767
768This library is free software. You can redistribute it and/or modify
769it under the same terms as Perl itself.
770
771=cut
772
773__PACKAGE__->meta->make_immutable;
774
7751;