LibreCat/LibreCat

View on GitHub
lib/LibreCat/App/Catalogue/Route/publication.pm

Summary

Maintainability
Test Coverage
package LibreCat::App::Catalogue::Route::publication;

=head1 NAME LibreCat::App::Catalogue::Route::publication

Route handler for publications.

=cut

use Catmandu::Sane;
use Catmandu;
use LibreCat qw(:self publication searcher);
use Catmandu::Fix qw(expand);
use Catmandu::Util qw(is_instance :is);
use LibreCat::App::Helper;
use LibreCat::App::Catalogue::Controller::Permission;
use Dancer qw(:syntax);
use Dancer::Plugin::FlashMessage;
use Encode qw(encode);
use File::Spec;
use URI::Escape qw(uri_escape);

sub access_denied_hook {
    h->hook('publication-access-denied')
        ->fix_around({_id => params->{id}, user_id => session->{user_id},});
}

sub decode_file {

    my $file = $_[0];

    $file = [] unless defined $file;

    #a single file was sent
    $file = [ $file ] if is_string( $file );

    #a list of files were sent. Make sure this hook does not break a correct record.file
    $file = [
        map {
            is_string( $_ ) ? from_json( encode("utf8",$_) ) : $_;
        } @$file
    ];

    $file;

}

=head1 PREFIX /librecat/record

All actions related to a publication record are handled under this prefix.

=cut

prefix '/librecat/record' => sub {

=head2 GET /new

Prints a list of available publication types + import form.

Some fields are pre-filled.

=cut

    get '/new' => sub {
        my $params_query= params("query");
        my $h           = h();
        my $type        = $params_query->{type};
        my $user_id     = session("user_id");
        my $user_login  = session("user");
        my $user_role   = session("role");
        my $user        = $h->current_user;

        return template 'backend/add_new' unless $type;

        # Need to generate a new publication identifier to be
        # able to load files associated with this new record...
        my $id = publication->generate_id;

        # set some basic values
        my $data = {
            _id        => $id,
            type       => $type,
            department => $user->{department},
            creator => { id => $user_id, login => $user_login },
            user_id => $user_id,
        };

        # Use config/hooks.yml to register functions
        # that should run before/after adding new publications
        # E.g. create a hooks to change the default fields
        state $hook = $h->hook('publication-new');

        $hook->fix_before($data);

        #-- Fill out default fields ---

        if ( $user_role eq "user") {
            my $person = {
                first_name => $user->{first_name},
                last_name  => $user->{last_name},
                full_name  => $user->{full_name},
                id         => $user_id,
            };
            $person->{orcid} = $user->{orcid} if $user->{orcid};

            if (   $type eq "bookEditor"
                or $type eq "conferenceEditor"
                or $type eq "journalEditor")
            {
                $data->{editor}->[0] = $person;
            }
            elsif ($type eq "translation" or $type eq "translationChapter") {
                $data->{translator}->[0] = $person;
            }
            else {
                $data->{author}->[0] = $person;
            }
        }

        if( $h->locale_exists( $params_query->{lang} ) ){
            $data->{lang} = $params_query->{lang};
        }

        # -- end default fields ---

        $hook->fix_after($data);

        # Important values and flags for the form in order to distinguish between the contexts
        # it is used in
        var form_action => uri_for( "/librecat/record" );
        var form_method => "POST";
        var new_record  => 1;

        my $template = File::Spec->catfile(
            "backend","forms",$type
        );

        template $template, $data;
    };

=head2 GET /edit/:id

Displays record for id.

Checks if the user has permission the see/edit this record.

=cut

    get '/edit/:id' => sub {

        my $id  = params("route")->{id};
        my $rec = publication->get($id) or pass;

        unless (
            p->can_edit(
                $rec->{_id},
                {user_id => session("user_id"), role => session("role")}
            )
            )
        {
            access_denied_hook();
            status '403';
            forward '/access_denied', {referer => request->referer};
        }

        # Use config/hooks.yml to register functions
        # that should run before/after edit publications
        state $hook = h->hook('publication-edit');

        $hook->fix_before($rec);

        my $template = File::Spec->catfile(
            "backend","forms", $rec->{type}
        );

        $rec->{return_url} = request->referer if request->referer;

        # --- End setting edit mode

        $hook->fix_after($rec);

        # Important values and flags for the form in order to distinguish between the contexts
        # it is used in
        var form_action => uri_for(
            "/librecat/record/".uri_escape($id),{ "x-tunneled-method" => "PUT" }
        );
        var form_method => "POST";
        var new_record  => 0;

        template $template, $rec;
    };

=head2 POST /update

Deprecated route: all trafic is internally forwarded to

* C<POST "/librecat/record"> when body parameter C<new_record> is given, or when body parameter C<_id> is missing.

* C<PUT "/librecat/record/:id"> when body parameter C<_id> is given and body parameter C<new_record> is missing.

=cut

    post '/update' => sub {
        my $params_body = params("body");

        if( $params_body->{new_record} ){

            forward "/librecat/record",{},{ method => "POST" };

        }

        if( is_string( $params_body->{_id} ) ){

            forward "/librecat/record/".uri_escape( $params_body->{_id} ),{},{ method => "PUT" };

        }

        forward "/librecat/record",{},{ method => "POST" };

    };

=head2 GET /return/:id

Set status to 'returned'.

Checks if the user has the rights to edit this record.

=cut

    get '/return/:id' => sub {
        my $return_url = params->{return_url};

        my $rec = publication->get(param("id")) or pass;

        unless (
            p->can_return(
                $rec->{_id},
                {user_id => session("user_id"), role => session("role")}
            )
            )
        {
            access_denied_hook();
            status '403';
            forward '/access_denied';
        }

        $rec->{user_id} = session("user_id");

        # Use config/hooks.yml to register functions
        # that should run before/after returning publications
        h->hook('publication-return')->fix_around(
            $rec,
            sub {
                $rec->{status} = "returned";
                publication->add($rec);
            }
        );

        redirect $return_url || uri_for('/librecat');
    };

=head2 GET /delete/:id

Deletes record with id. For admins only.

=cut

    get '/delete/:id' => sub {
        my $id = params->{id};

        my $rec = publication->get($id);

        unless (
            p->can_delete(
                $rec->{_id},
                {user_id => session("user_id"), role => session("role")}
            )
            )
        {
            access_denied_hook();
            status '403';
            forward '/access_denied';
        }

        $rec->{user_id} = session->{user_id};

        # Use config/hooks.yml to register functions
        # that should run before/after deleting publications
        h->hook('publication-delete')->fix_around(
            $rec,
            sub {
                publication->delete($id);
            }
        );

        redirect uri_for('/librecat');
    };

=head2 GET /preview/:id.:fmt

Export publication with ID :id in format :fmt

Only publications with status C<deleted> are not visible.

=cut

get '/preview/:id.:fmt' => sub {
    my $rparams = params("route");
    my $id  = $rparams->{id};
    my $fmt = $rparams->{fmt} // 'yaml';

    forward "/librecat/export", {cql => "id=$id", fmt => $fmt , limit => 1};
};

=head2 GET /preview/id

Prints the frontdoor for every record.

=cut

    get '/preview/:id' => sub {
        my $id = params->{id};

        my $hits = publication->get($id);

        $hits->{style}  = h->config->{citation}->{csl}->{default_style};
        $hits->{marked} = 0;

        template 'publication/record.tt', $hits;
    };

=head2 GET /internal_view/:id

Prints internal view, optionally as data dumper.

For admins only!

=cut

    get '/internal_view/:id' => sub {
        my $id = params->{id};

        my $rec; my $hits;
        if(params->{searcher}){
          $rec = publication->search_bag->get($id);
        }
        else {
          $rec = publication->get($id);
        }

        unless ($rec) {
            return template 'error',
                {message => "No publication found with ID $id."};
        }

        my $export_string;
        my $exporter = Catmandu->exporter('YAML', file => \$export_string);
        $exporter->add($rec);

        $export_string = Encode::encode( 'UTF-8', $export_string );

        my %headers = (
            'Content-Type'   => 'text/plain' ,
            'Content-Length' => length($export_string) ,
        );

        Dancer::Response->new(
           status => 200,
           content => $export_string,
           encoded => 1,
           headers => [%headers],
           forward => ""
       );
    };

=head2 GET /clone/:id

Clones the record with ID :id and returns a form with a different ID.

=cut

    get '/clone/:id' => sub {
        my $id  = params("route")->{id};
        my $rec = publication->get($id);

        unless ($rec) {
            return template 'error',
                {message => "No publication found with ID $id."};
        }

        delete $rec->{_version};
        delete $rec->{date_created};
        delete $rec->{date_updated};
        delete $rec->{urn};
        delete $rec->{doi};
        delete $rec->{file};
        delete $rec->{related_material};

        $rec->{_id}     = publication->generate_id;
        $rec->{status}  = "new";
        $rec->{creator} = {id => session("user_id"), login => session("user")};

        #important values and flags for the form in order to distinguish between the contexts
        #it is used in
        var form_action => uri_for( "/librecat/record" );
        var form_method => "POST";
        var new_record  => 1;

        my $template = File::Spec->catfile(
            "backend","forms",$rec->{type}
        );

        template $template, $rec;
    };

=head2 GET /publish/:id

Publishes private records, returns to the list.

=cut

    get '/publish/:id' => sub {
        my $return_url = params->{return_url};

        my $rec = publication->get(param("id")) or pass;

        unless (
            p->can_make_public(
                $rec->{_id},
                {user_id => session("user_id"), role => session("role")}
            )
            )
        {
            access_denied_hook();
            status '403';
            forward '/access_denied';
        }

        my $old_status = $rec->{status};

        if (session->{role} eq "super_admin") {
            $rec->{status} = "public";
        }
        elsif ($rec->{type} eq "research_data") {
            $rec->{status} = "submitted" if $old_status eq "private";
        }
        else {
            $rec->{status} = "public";
        }

        if ($rec->{status} ne $old_status) {

            # Use config/hooks.yml to register functions
            # that should run before/after publishing publications
            state $hook = h->hook('publication-publish');

            $hook->fix_before($rec);

            publication->add($rec);

            $hook->fix_after($rec);
        }

        redirect $return_url || uri_for('/librecat');
    };

=head2 POST /change_type

Changes the type of the publication.

The record is not stored yet.

=cut

    post '/change_type' => sub {
        my $params_body = params("body");
        my $h = h();

        #unpack strange format of record.file
        #TODO: this should not be necessary
        $params_body->{file} = decode_file( $params_body->{file} );

        my $body = $h->nested_params( $params_body );

        # Use config/hooks.yml to register functions
        # that should run before/after changing the edit mode
        state $hook = $h->hook('publication-change-type');
        $hook->fix_before($body);
        $hook->fix_after($body);

        # Important values and flags for the form in order to distinguish between the contexts
        # it is used in
        if( publication()->get( $body->{_id} ) ){

            var form_action => uri_for(
                "/librecat/record/".uri_escape( $body->{_id} ),{ "x-tunneled-method" => "PUT" }
            );
            var form_method => "POST";
            var new_record  => 0;

        }
        else {

            var form_action => uri_for( "/librecat/record" );
            var form_method => "POST";
            var new_record  => 1;

        }

        my $template = File::Spec->catfile(
            "backend","forms",$body->{type}
        );

        template $template, $body;
    };

=head2 POST /

Saves a new record in the database.

=cut

    post "/" => sub {

        my $params_query = params("query");
        my $params_body  = params("body");
        my $request      = request();
        my $return_url   = is_string( $params_body->{return_url} ) ?
            $params_body->{return_url} : $request->uri_for("/librecat");
        delete $params_body->{return_url};
        my $h = h();
        my $librecat = librecat();
        my $model = publication();

        $h->log->debug( "Body parameters:" . to_dumper($params_body) );

        #record should not be present
        if( $model->get( $params_body->{_id} ) ){

            flash danger => $h->localize( "error.record_id_taken", $params_body->{_id} );
            return redirect $return_url;

        }

        # When the form isn't fully loaded when the record is saved bail out and cry for help
        if( $params_body->{_end_} ne "_end_" ){
            flash danger => $h->localize("error.preliminary_submit");
            return redirect $return_url;
        }
        delete $params_body->{_end_};

        # Unpack strange format of record.file
        # TODO: this should not be necessary
        $params_body->{file} = decode_file( $params_body->{file} );

        my $body = $h->nested_params( $params_body );

        # User that last updated this record
        $body->{user_id} = session("user_id");

        # This used to live in the form..
        $body->{status} = "private";

        # Use config/hooks.yml to register functions
        # that should run before/after updating publications
        my @error_messages;

        try {
            $h->hook("publication-create")->fix_around(
                $body,
                sub {
                    $model->add(
                        $body,
                        on_validation_error => sub {
                            my($rec, $errors) = @_;
                            $librecat->log->errorf(
                                "%s not a valid publication %s",
                                $rec->{_id},
                                [map { $_->localize() } @$errors]
                            );
                            my $current_locale = $h->locale();
                            @error_messages  = map {
                                $_->localize( $current_locale );
                            } @$errors;
                        }
                    );
                }
            );
        }
        catch {

            $h->log->fatal("failed to create record");
            $h->log->fatal($_);

            push @error_messages,
                $h->localize( "error.create_failed", $body->{_id} ) . " " .
                $h->localize( "error.contact_admin",$h->config->{admin_email} );

        };

        # All is well
        return redirect $return_url if scalar( @error_messages ) == 0;

        # When we have an error record we return to the edit form and show
        # all errors...
        my $template = File::Spec->catfile(
            "backend","forms", $body->{type}
        );

        flash danger => join( "<br>", @error_messages );

        # Important values and flags for the form in order to distinguish between the contexts
        # it is used in
        var form_action => $request->uri_for( "/librecat/record" );
        var form_method => "POST";
        var new_record  => 1;

        template $template, $body;

    };

=head2 PUT /:id

Updates an existing record in the database

Checks if the user has the rights to update this record.

All data must be supplied

If record does not exist, then this route does not match

=cut

    put "/:id" => sub {

        my $id = params("route")->{id};
        my $params_query = params("query");
        my $params_body  = params("body");
        my $request      = request();
        my $return_url   = is_string( $params_body->{return_url} ) ?
            $params_body->{return_url} : $request->uri_for("/librecat");
        delete $params_body->{return_url};
        my $h       = h();
        my $p       = p();
        my $model   = publication();
        my $librecat = librecat();

        #record not found
        pass unless $model->get( $id );

        $h->log->debug( "Body parameters:" . to_dumper($params_body) );

        # When the form isn't fully loaded when the record is saved bail out and cry for help
        if( $params_body->{_end_} ne "_end_" ){
            flash danger => $h->localize("error.preliminary_submit");
            return redirect $return_url;
        }
        delete $params_body->{_end_};

        # Unpack strange format of record.file
        # TODO: this should not be necessary
        $params_body->{file} = decode_file( $params_body->{file} );

        my $body = $h->nested_params( $params_body );

        # Just to make sure..
        $body->{_id} = $id;

        # User that last updated this record
        $body->{user_id} = session("user_id");

        my $finalSubmit = delete $body->{finalSubmit};
        $finalSubmit    = is_string( $finalSubmit ) ? $finalSubmit : "";

        if(
            $finalSubmit eq "recPublish" &&
            $p->can_make_public(
                $id,
                { user_id => session("user_id"), role => session("role")}
            )
        ){
            # ok
        }
        elsif(
            $finalSubmit eq "recReturn" &&
            $p->can_return(
                $id,
                { user_id => session("user_id"), role => session("role")}
            )
        ){
            # ok
        }
        elsif(
            $finalSubmit eq "recSubmit" && $p->can_submit(
                $id,
                { user_id => session("user_id"), role => session("role")}
            )
        ){
            # ok
        }
        elsif(
            $p->can_edit(
                $id,
                { user_id => session("user_id"), role => session("role")}
            )
        ){
            # ok
        }
        else {
            access_denied_hook();
            status "403";
            forward "/access_denied";
        }

        if( $finalSubmit eq "" ){
            $librecat->log->warn("receiving an empty finalSubmit from the form");
        }
        elsif( $finalSubmit eq "recSubmit" ){
            $body->{status} = "submitted";
        }
        elsif( $finalSubmit eq "recPublish" ){
            $body->{status} = "public";
        }
        elsif( $finalSubmit eq "recReturn" ){
            $body->{status} = "returned";
        }
        else{
            $librecat->log->warnf(
                "receiving an unknown finalSubmit `%s` from the form", $finalSubmit
            );
        }

        # Use config/hooks.yml to register functions
        # that should run before/after updating publications
        my @error_messages;

        try {
            $h->hook("publication-update")->fix_around(
                $body,
                sub {
                    publication->add(
                        $body,
                        on_validation_error => sub {
                            my($rec, $errors) = @_;
                            $librecat->log->errorf(
                                "%s not a valid publication %s",
                                $id,
                                [map { $_->localize() } @$errors]
                            );
                            my $current_locale = $h->locale();
                            @error_messages  = map {
                                $_->localize( $current_locale );
                            } @$errors;
                        }
                    );
                }
            );
        }
        catch {

            if( is_instance($_, "LibreCat::Error::VersionConflict") ){
                flash warning => $h->localize("error.version_conflict");
            }
            else{

                $h->log->fatal("failed to update record $id");
                $h->log->fatal($_);

                push @error_messages,
                    $h->localize( "error.update_failed",$id ) . " " .
                    $h->localize( "error.contact_admin",$h->config->{admin_email} );

            }

        };

        # All is well
        return redirect( $return_url ) if scalar( @error_messages ) == 0;

        # When we have an error record we return to the edit form and show
        # all errors...
        my $template = File::Spec->catfile(
            "backend","forms", $body->{type}
        );

        flash danger => join( "<br>", @error_messages );

        # Important values and flags for the form in order to distinguish between the contexts
        # it is used in
        var form_action => $request->uri_for(
            "/librecat/record/".uri_escape($id),{ "x-tunneled-method" => "PUT" }
        );
        var form_method => "POST";
        var new_record  => 0;

        template $template, $body;

    };

};

1;