Bug 29939: Use the REST API for ratings
authorJonathan Druart <jonathan.druart@bugs.koha-community.org>
Tue, 16 Aug 2022 10:14:06 +0000 (12:14 +0200)
committerTomas Cohen Arazi <tomascohen@theke.io>
Mon, 22 Aug 2022 14:31:15 +0000 (11:31 -0300)
This patch replaces opac-ratings-ajax.pl with a new REST API route
POST /public/biblios/42/ratings

Note that we could go further and refactor the 'start_rating' select
code.

Test plan:
Test the "star ratings" feature at the OPAC, on the different page
where it's displayed.

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Koha/REST/V1/Biblios.pm
api/v1/swagger/paths/biblios.yaml
api/v1/swagger/swagger.yaml
koha-tmpl/opac-tmpl/bootstrap/en/includes/doc-head-close.inc
koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-detail.tt
koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-readingrecord.tt
koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt
koha-tmpl/opac-tmpl/bootstrap/js/global.js
koha-tmpl/opac-tmpl/bootstrap/js/ratings.js
opac/opac-ratings-ajax.pl [deleted file]
t/db_dependent/api/v1/biblios.t

index c74f518..f923255 100644 (file)
@@ -20,6 +20,7 @@ use Modern::Perl;
 use Mojo::Base 'Mojolicious::Controller';
 
 use Koha::Biblios;
+use Koha::Ratings;
 use Koha::RecordProcessor;
 use C4::Biblio qw( DelBiblio );
 
@@ -409,4 +410,74 @@ sub get_items_public {
     };
 }
 
+=head3 set_rating
+
+Set rating for the logged in user
+
+=cut
+
+
+sub set_rating {
+    my $c = shift->openapi->valid_input or return;
+
+    my $biblio = Koha::Biblios->find( $c->validation->param('biblio_id') );
+
+    unless ($biblio) {
+        return $c->render(
+            status  => 404,
+            openapi => {
+                error => "Object not found."
+            }
+        );
+    }
+
+    my $patron = $c->stash('koha.user');
+    unless ($patron) {
+        return $c->render(
+            status => 403,
+            openapi =>
+                { error => "Cannot rate. Reason: must be logged-in" }
+        );
+    }
+
+    my $body   = $c->validation->param('body');
+    my $rating_value = $body->{rating};
+
+    return try {
+
+        my $rating = Koha::Ratings->find(
+            {
+                biblionumber   => $biblio->biblionumber,
+                borrowernumber => $patron->borrowernumber,
+            }
+        );
+        $rating->delete if $rating;
+
+        if ( $rating_value ) { # Cannot set to 0 from the UI
+            $rating = Koha::Rating->new(
+                {
+                    biblionumber   => $biblio->biblionumber,
+                    borrowernumber => $patron->borrowernumber,
+                    rating_value   => $rating_value,
+                }
+            )->store;
+        };
+        my $ratings =
+          Koha::Ratings->search( { biblionumber => $biblio->biblionumber } );
+        my $average = $ratings->get_avg_rating;
+
+        return $c->render(
+            status  => 200,
+            openapi => {
+                rating  => $rating && $rating->in_storage ? $rating->rating_value : undef,
+                average => $average,
+                count   => $ratings->count
+            },
+        );
+    }
+    catch {
+        $c->unhandled_exception($_);
+    };
+}
+
 1;
index 60e4e95..782de21 100644 (file)
         description: Under maintenance
         schema:
           $ref: "../swagger.yaml#/definitions/error"
+"/public/biblios/{biblio_id}/ratings":
+  post:
+    x-mojo-to: Biblios#set_rating
+    operationId: setBiblioRating
+    tags:
+      - biblios
+    summary: set biblio rating (public)
+    parameters:
+      - $ref: "../swagger.yaml#/parameters/biblio_id_pp"
+      - name: body
+        in: body
+        description: A JSON object containing rating information
+        schema:
+          type: object
+          properties:
+            rating:
+              description: the rating
+              type:
+                - integer
+                - "null"
+          required:
+              - rating
+          additionalProperties: false
+    produces:
+      - application/json
+    responses:
+      "200":
+        description: Rating set
+        schema:
+          type: object
+          properties:
+            rating:
+              description: user's rating
+              type:
+                - number
+                - "null"
+            average:
+              description: average rating
+              type: number
+            count:
+              description: number of ratings
+              type: integer
+          additionalProperties: false
+      "401":
+        description: Authentication required
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      "403":
+        description: Access forbidden
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      "404":
+        description: Biblio not found
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      "500":
+        description: |
+          Internal server error. Possible `error_code` attribute values:
+
+          * `internal_server_error`
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
+      "503":
+        description: Under maintenance
+        schema:
+          $ref: "../swagger.yaml#/definitions/error"
index 597af62..a10f240 100644 (file)
@@ -207,6 +207,8 @@ paths:
     $ref: "./paths/biblios.yaml#/~1public~1biblios~1{biblio_id}"
   "/public/biblios/{biblio_id}/items":
     $ref: "./paths/biblios.yaml#/~1public~1biblios~1{biblio_id}~1items"
+  "/public/biblios/{biblio_id}/ratings":
+    $ref: "./paths/biblios.yaml#/~1public~1biblios~1{biblio_id}~1ratings"
   /public/libraries:
     $ref: ./paths/libraries.yaml#/~1public~1libraries
   "/public/libraries/{library_id}":
index e29568d..816f4a8 100644 (file)
@@ -63,6 +63,7 @@
 <script>
     var Koha = {};
     function _(s) { return s } // dummy function for gettext
+    const is_logged_in = [% IF logged_in_user %]true[% ELSE %]false[% END %]
 </script>
 [% IF lang && lang != 'en' %]
     [% Asset.js(lang _ '/js/locale_data.js') | $raw %]
index 802a642..e69c3fa 100644 (file)
                     [% IF ( OpacStarRatings != 'disable' ) %]
                         <form method="post" action="/cgi-bin/koha/opac-ratings.pl">
                             <legend class="sr-only">Star ratings</legend>
-                            <div class="results_summary ratings">
+                            <div class="results_summary ratings" id="rating-[% biblio.biblionumber | html %]">
 
                                 [% SET rating_avg = ratings.get_avg_rating() %]
                                 [% rating_avg_int = BLOCK %][% rating_avg | format("%.0f") %][% END %]
 
                                 [% IF ( borrowernumber ) %]
-                                    <select id="star_rating" name="rating" data-rating-enabled="1" autocomplete="off">
+                                    <select id="star_rating" class="star_rating" name="rating" data-biblionumber="[% biblio.biblionumber | html %]" data-context="rating-[% biblio.biblionumber | html %]" autocomplete="off">
                                 [% ELSE %]
-                                    <select id="star_rating" name="rating" data-rating-enabled="0" disabled="disabled" autocomplete="off">
+                                    <select id="star_rating" class="star_rating" name="rating" disabled="disabled" autocomplete="off">
                                 [% END %]
                                     [% IF ( rating_avg_int == 0 ) %]
                                         <option value="" selected="selected"></option>
                                         [% END %]
                                     [% END %]
                                 </select>
-                                <img id="rating-loading" style="display:none" src="[% interface | html %]/[% theme | html %]/images/spinner-small.gif" alt="" />
+                                <img id="rating-loading" class="rating-loading" style="display:none" src="[% interface | html %]/[% theme | html %]/images/spinner-small.gif" alt="" />
 
                                 <!-- define some hidden vars for ratings -->
 
                                 <input  type="hidden" name='biblionumber'  value="[% biblio.biblionumber | html %]" />
-                                <input  type="hidden" name='rating_value' id='rating_value' value="[% my_rating.rating_value | html %]" />
+                                <input  type="hidden" name='rating_value' id='rating_value' class="rating_value" value="[% my_rating.rating_value | html %]" />
 
                                 [% UNLESS ( rating_readonly ) %]&nbsp;  <input name="rate_button" type="submit" value="Rate me" />[% END %]&nbsp;
 
                                 [% IF my_rating %]
-                                    <span id="rating_value_text">Your rating: [% my_rating.rating_value | html %].</span>
-                                    <span id="cancel_rating_text"><a href="#"><i class="fa fa-remove" aria-hidden="true"></i> Cancel rating</a>.</span>
+                                    <span id="rating_value_text" class="rating_value_text">Your rating: [% my_rating.rating_value | html %].</span>
+                                    <span id="cancel_rating_text" class="cancel_rating_text"><a href="#" data-context="star_rating"><i class="fa fa-remove" aria-hidden="true"></i> Cancel rating.</a></span>
                                 [% ELSE %]
-                                    <span id="rating_value_text"></span>
-                                    <span id="cancel_rating_text" style="display:none;"><a href="#"><i class="fa fa-remove" aria-hidden="true"></i> Cancel rating</a>.</span>
+                                    <span id="rating_value_text" class="rating_value_text"></span>
+                                    <span id="cancel_rating_text" class="cancel_rating_text" style="display: none;"><a href="#" data-context="star_rating"><i class="fa fa-remove" aria-hidden="true"></i> Cancel rating.</a></span>
                                 [% END %]
 
-                                <span id="rating_text">Average rating: [% rating_avg | html %] ([% ratings.count | html %] votes)</span>
+                                <span id="rating_text" class="rating_text">Average rating: [% rating_avg | html %] ([% ratings.count | html %] votes)</span>
                             </div>
                         </form>
                     [% END # / IF OpacStarRatings != 'disable' %]
     [% INCLUDE 'datatables.inc' %]
     [% INCLUDE 'columns_settings.inc' %]
     [% INCLUDE greybox.inc %]
-    [% IF ( OpacStarRatings != 'disable' ) %][% Asset.js("lib/jquery/plugins/jquery.barrating.min.js") | $raw %][% END %]
+    [% IF ( OpacStarRatings != 'disable' ) %]
+        [% Asset.js("lib/jquery/plugins/jquery.barrating.min.js") | $raw %]
+        [% Asset.js("js/ratings.js") | $raw %]
+    [% END %]
 
     [% IF ( OpacHighlightedWords ) %][% Asset.js("lib/jquery/plugins/jquery.highlight-3.js") | $raw %][% END %]
     [% IF ( Koha.Preference('OPACDetailQRCode') ) %]
                     });
                 }());
             [% END # /IF ( OPACShelfBrowser ) %]
-
-            [% IF ( OpacStarRatings != 'disable' ) %]
-                // -----------------------------------------------------
-                // star-ratings code
-                // -----------------------------------------------------
-                // hide 'rate' button if javascript enabled
-
-                $('input[name="rate_button"]').remove();
-
-                var rating_enabled = ( $("#star_rating").data("rating-enabled") == "1" ) ? false : true;
-                $('#star_rating').barrating({
-                    theme: 'fontawesome-stars',
-                    showSelectedRating: false,
-                    allowEmpty: true,
-                    deselectable: false,
-                    readonly: rating_enabled,
-                    onSelect: function(value, text) {
-                        $("#rating-loading").show();
-                        $.post("/cgi-bin/koha/opac-ratings-ajax.pl", {
-                            rating_old_value: $("#rating_value").attr("value"),
-                            borrowernumber: "[% borrowernumber | html %]",
-                            biblionumber: "[% biblio.biblionumber | html %]",
-                            rating_value: value,
-                            auth_error: value
-                        }, function (data) {
-                                $("#rating_value").val(data.rating_value);
-                                if (data.rating_value) {
-                                    $("#rating_value_text").text(_("Your rating: %s, ").format(data.rating_value));
-                                    $("#cancel_rating_text").show();
-                                } else {
-                                    $("#rating_value_text").text('');
-                                    $("#cancel_rating_text").hide();
-                                }
-                                $("#rating_text").text(_("Average rating: %s (%s votes)").format(data.rating_avg, data.rating_total));
-                                $("#rating-loading").hide();
-                        }, "json");
-                    }
-                });
-
-                $("#cancel_rating_text a").on("click", function(e){
-                    e.preventDefault();
-                    $("#star_rating").barrating("set", "");
-                });
-
-            [% END # / IF ( OpacStarRatings != 'disable' )%]
         });
 
         $(document).ready(function() {
index dc5e2aa..1e4f7d6 100644 (file)
             }
         });
     });
-    var borrowernumber = "[% logged_in_user.borrowernumber | html %]";
-    var MSG_YOUR_RATING = _("Your rating: %s, ");
-    var MSG_AVERAGE_RATING = _("Average rating: %s (%s votes)");
 </script>
     [% IF ( Koha.Preference('OpacStarRatings') == 'all' ) %]
         [% Asset.js("lib/jquery/plugins/jquery.barrating.min.js") | $raw %]
index 11275fa..d131abc 100644 (file)
         }
 
         var borrowernumber = "[% borrowernumber | html %]";
-        var MSG_YOUR_RATING = _("Your rating: %s, ");
-        var MSG_AVERAGE_RATING = _("Average rating: %s (%s votes)");
     </script>
     [% IF ( Koha.Preference('OpacStarRatings') == 'all' ) %]
         [% Asset.js("lib/jquery/plugins/jquery.barrating.min.js") | $raw %]
index 1a40118..724d9d4 100644 (file)
@@ -117,6 +117,18 @@ function confirmModal(message, title, yes_label, no_label, callback) {
     $("#bootstrap-confirm-box-modal").modal('show');
 }
 
+
+// Function to check errors from AJAX requests
+const checkError = function(response) {
+    if (response.status >= 200 && response.status <= 299) {
+        return response.json();
+    } else {
+        console.log("Server returned an error:");
+        console.log(response);
+        alert("%s (%s)".format(response.statusText, response.status));
+    }
+};
+
 //Add jQuery :focusable selector
 (function($) {
     function visible(element) {
index 971e604..f867d6d 100644 (file)
@@ -1,4 +1,3 @@
-/* global borrowernumber MSG_YOUR_RATING MSG_AVERAGE_RATING */
 // -----------------------------------------------------
 // star-ratings code
 // -----------------------------------------------------
@@ -14,27 +13,35 @@ $(document).ready(function(){
         showSelectedRating: false,
         allowEmpty: true,
         deselectable: false,
+        readonly: !is_logged_in,
         onSelect: function( value ) {
             var context = $("#" + this.$elem.data("context") );
             $(".rating-loading", context ).show();
-            $.post("/cgi-bin/koha/opac-ratings-ajax.pl", {
-                rating_old_value: $(".rating_value", context ).attr("value"),
-                borrowernumber: borrowernumber,
-                biblionumber: this.$elem.data('biblionumber'),
-                rating_value: value,
-                auth_error: value
-            }, function (data) {
-                $(".rating_value", context ).val(data.rating_value);
-                if (data.rating_value) {
-                    $(".rating_value_text", context ).text( MSG_YOUR_RATING.format(data.rating_value) );
+            let biblionumber = this.$elem.data('biblionumber');
+            if ( value == "" ) value = null;
+            fetch("/api/v1/public/biblios/"+biblionumber+"/ratings", {
+                method: 'POST',
+                body: JSON.stringify({ rating: value }),
+                headers: {
+                    "Content-Type": "application/json;charset=utf-8",
+                }
+            }).then(checkError)
+              .then((data) => {
+                $(".rating_value", context ).val(data.rating);
+                console.log(data);
+                  console.log($(".cancel_rating_text", context ));
+                if (data.rating) {
+                    console.log(data.rating);
+                    $(".rating_value_text", context ).text( __("Your rating: %s.").format(data.rating) );
                     $(".cancel_rating_text", context ).show();
                 } else {
                     $(".rating_value_text", context ).text("");
                     $(".cancel_rating_text", context ).hide();
                 }
-                $(".rating_text", context ).text( MSG_AVERAGE_RATING.format(data.rating_avg, data.rating_total) );
+                  console.log($(".rating_text", context ));
+                $(".rating_text", context ).text( __("Average rating: %s (%s votes)").format(data.average, data.count) );
                 $(".rating-loading", context ).hide();
-            }, "json");
+            });
         }
     });
 
diff --git a/opac/opac-ratings-ajax.pl b/opac/opac-ratings-ajax.pl
deleted file mode 100755 (executable)
index dcbeaeb..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-#!/usr/bin/perl
-
-# Copyright 2011 KohaAloha, NZ
-#
-# This file is part of Koha.
-#
-# Koha is free software; you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 3 of the License, or
-# (at your option) any later version.
-#
-# Koha is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Koha; if not, see <http://www.gnu.org/licenses>.
-
-=head1 DESCRIPTION
-
-A script that takes an ajax json query, and then inserts or modifies a star-rating.
-
-=cut
-
-use Modern::Perl;
-
-use CGI qw ( -utf8 );
-use CGI::Cookie;  # need to check cookies before having CGI parse the POST request
-
-use C4::Auth qw( get_template_and_user check_cookie_auth );
-use C4::Context;
-use C4::Output qw( is_ajax output_ajax_with_http_headers );
-
-use Koha::Ratings;
-
-use JSON;
-
-my $is_ajax = is_ajax();
-
-my ( $query, $auth_status );
-if ($is_ajax) {
-    ( $query, $auth_status ) = &ajax_auth_cgi( {} );
-}
-else {
-    $query = CGI->new();
-}
-
-my $biblionumber     = $query->param('biblionumber');
-my $rating_value     = $query->param('rating_value');
-my $rating_old_value = $query->param('rating_old_value');
-
-my ( $template, $loggedinuser, $cookie );
-if ($is_ajax) {
-    $loggedinuser = C4::Context->userenv->{'number'};
-}
-else {
-    ( $template, $loggedinuser, $cookie ) = get_template_and_user(
-        {
-            template_name   => "opac-detail.tt",
-            query           => $query,
-            type            => "opac",
-            authnotrequired => 0,                    # auth required to add tags
-        }
-    );
-}
-
-my $rating;
-$rating_value //= '';
-
-if ( $rating_value eq '' ) {
-    my $rating = Koha::Ratings->find( { biblionumber => $biblionumber, borrowernumber => $loggedinuser } );
-    $rating->delete if $rating;
-}
-
-elsif ( $rating_value and !$rating_old_value ) {
-    Koha::Rating->new( { biblionumber => $biblionumber, borrowernumber => $loggedinuser, rating_value => $rating_value, })->store;
-}
-
-elsif ( $rating_value ne $rating_old_value ) {
-    my $rating = Koha::Ratings->find( { biblionumber => $biblionumber, borrowernumber => $loggedinuser });
-    $rating->rating_value($rating_value)->store if $rating
-}
-
-my $ratings = Koha::Ratings->search({ biblionumber => $biblionumber });
-my $my_rating = $ratings->search({ borrowernumber => $loggedinuser })->next;
-my $avg = $ratings->get_avg_rating;
-
-my %js_reply = (
-    rating_total   => $ratings->count,
-    rating_avg     => $avg,
-    rating_avg_int => sprintf("%.0f", $avg),
-    rating_value   => $my_rating ? $my_rating->rating_value : undef,
-    auth_status    => $auth_status,
-
-);
-
-my $json_reply = JSON->new->encode( \%js_reply );
-
-#### $rating
-#### %js_reply
-#### $json_reply
-
-output_ajax_with_http_headers( $query, $json_reply );
-exit;
-
-# a ratings specific ajax return sub, returns CGI object, and an 'auth_success' value
-sub ajax_auth_cgi {
-    my $needed_flags = shift;
-    my %cookies      = CGI::Cookie->fetch;
-    my $input        = CGI->new;
-    my $sessid = $cookies{'CGISESSID'}->value || $input->param('CGISESSID');
-    my ( $auth_status ) =
-      check_cookie_auth( $sessid, $needed_flags );
-    return $input, $auth_status;
-}
index ca7a23e..b4e98b1 100755 (executable)
@@ -20,7 +20,7 @@ use Modern::Perl;
 use utf8;
 use Encode;
 
-use Test::More tests => 7;
+use Test::More tests => 8;
 use Test::MockModule;
 use Test::Mojo;
 use Test::Warn;
@@ -676,3 +676,40 @@ subtest 'get_checkouts() tests' => sub {
 
     $schema->storage->txn_rollback;
 };
+
+subtest 'set_rating() tests' => sub {
+
+    plan tests => 12;
+
+    $schema->storage->txn_begin;
+
+    my $patron = $builder->build_object(
+        {
+            class => 'Koha::Patrons',
+            value => { flags => 0 }
+        }
+    );
+    my $password = 'thePassword123';
+    $patron->set_password( { password => $password, skip_validation => 1 } );
+    $patron->discard_changes;
+    my $userid = $patron->userid;
+
+    my $biblio = $builder->build_sample_biblio();
+    $t->post_ok("/api/v1/public/biblios/" . $biblio->biblionumber . "/ratings" => json => { rating => 3 })
+      ->status_is(403);
+
+    $t->post_ok("//$userid:$password@/api/v1/public/biblios/" . $biblio->biblionumber . "/ratings" => json => { rating => 3 })
+      ->status_is(200)
+      ->json_is( '/rating', '3' )
+      ->json_is( '/average', '3' )
+      ->json_is( '/count', '1' );
+
+    $t->post_ok("//$userid:$password@/api/v1/public/biblios/" . $biblio->biblionumber . "/ratings" => json => { rating => undef })
+      ->status_is(200)
+      ->json_is( '/rating', undef )
+      ->json_is( '/average', '0' )
+      ->json_is( '/count', '0' );
+
+    $schema->storage->txn_rollback;
+
+};