feat: Customer Ratings & Reviews Full Page

- Created macros for repetitive snippets
- Created Customer Reviews full page
- View more button to reveal 10 more reviews at a time
- Common function to get reviews with start and end
This commit is contained in:
marination 2021-04-07 23:49:04 +05:30
parent b15ff57a66
commit c842305be0
12 changed files with 316 additions and 83 deletions

View File

@ -79,13 +79,14 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-03-24 22:27:28.094535",
"modified": "2021-04-02 15:56:00.447950",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Item Review",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
@ -93,9 +94,11 @@
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
@ -103,6 +106,17 @@
"read": 1,
"report": 1,
"role": "Website Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"report": 1,
"role": "Customer",
"share": 1
}
],

View File

@ -4,14 +4,48 @@
from __future__ import unicode_literals
from datetime import datetime
from six import string_types
import json
import frappe
from frappe.model.document import Document
from frappe.contacts.doctype.contact.contact import get_contact_name
from frappe.utils import flt, cint
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import get_shopping_cart_settings
class ItemReview(Document):
pass
@frappe.whitelist()
def get_item_reviews(web_item, start, end, data=None):
if not data:
data = frappe._dict()
settings = get_shopping_cart_settings()
if settings and settings.get("enable_reviews"):
data.reviews = frappe.db.get_all("Item Review", filters={"website_item": web_item},
fields=["*"], limit_start=cint(start), limit_page_length=cint(end))
rating_data = frappe.db.get_all("Item Review", filters={"website_item": web_item},
fields=["avg(rating) as average, count(*) as total"])[0]
data.average_rating = flt(rating_data.average, 1)
data.average_whole_rating = flt(data.average_rating, 0)
# get % of reviews per rating
reviews_per_rating = []
for i in range(1,6):
count = frappe.db.get_all("Item Review", filters={"website_item": web_item, "rating": i},
fields=["count(*) as count"])[0].count
percent = flt((count / rating_data.total or 1) * 100, 0) if count else 0
reviews_per_rating.append(percent)
data.reviews_per_rating = reviews_per_rating
data.total_reviews = rating_data.total
return data
@frappe.whitelist()
def add_item_review(web_item, title, rating, comment=None):
""" Add an Item Review by a user if non-existent. """

View File

@ -14,6 +14,7 @@ from frappe.utils import cstr, random_string, cint, flt
from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for)
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
class WebsiteItem(WebsiteGenerator):
website = frappe._dict(
@ -176,7 +177,7 @@ class WebsiteItem(WebsiteGenerator):
self.set_metatags(context)
self.set_shopping_cart_data(context)
self.get_product_details_section(context)
self.get_reviews(context)
get_item_reviews(self.name, 0, 4, context)
context.wished = False
if frappe.db.exists("Wishlist Items", {"item_code": self.item_code, "parent": frappe.session.user}):
@ -362,28 +363,6 @@ class WebsiteItem(WebsiteGenerator):
return tab_values
def get_reviews(self, context):
if context.shopping_cart.cart_settings.enable_reviews:
context.reviews = frappe.db.get_all("Item Review", filters={"item": self.item_code},
fields=["*"], limit=4)
rating_data = frappe.db.get_all("Item Review", filters={"item": self.item_code},
fields=["avg(rating) as average, count(*) as total"])[0]
context.average_rating = rating_data.average
context.average_whole_rating = flt(context.average_rating, 0)
# get % of reviews per rating
reviews_per_rating = []
for i in range(1,6):
count = frappe.db.get_all("Item Review", filters={"item": self.item_code, "rating": i},
fields=["count(*) as count"])[0].count
percent = flt((count / rating_data.total or 1) * 100, 0) if count else 0
reviews_per_rating.append(percent)
context.reviews_per_rating = reviews_per_rating
context.total_reviews = rating_data.total
def invalidate_cache_for_web_item(doc):
"""Invalidate Website Item Group cache and rebuild ItemVariantsCacheManager."""
from erpnext.stock.doctype.item.item import invalidate_item_variants_cache_for_website

View File

@ -67,7 +67,7 @@ class ProductQuery:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
if product_info:
item.formatted_price = (product_info.get('price') or {}).get('formatted_price')
item.price = product_info['price'].get('price_list_rate')
item.price = (product_info.get('price') or {}).get('price_list_rate')
if self.settings.show_stock_availability:
if item.get("website_warehouse"):

View File

@ -705,7 +705,6 @@ body.product-page {
.ratings-reviews-section {
border-top: 1px solid #E2E6E9;
display: flex;
}
.reviews-header {

View File

@ -170,7 +170,7 @@ def get_item_for_list_in_html(context):
def get_parent_item_groups(item_group_name, from_item=False):
base_nav_page = {"name": frappe._("Shop by Category"), "route":"/shop-by-category"}
if from_item:
if from_item and frappe.request.environ.get("HTTP_REFERER"):
# base page after 'Home' will vary on Item page
last_page = frappe.request.environ["HTTP_REFERER"].split('/')[-1]
if last_page and last_page in ("shop-by-category", "all-products"):

View File

@ -1,47 +1,16 @@
{% from "erpnext/templates/includes/macros.html" import ratings_with_title %}
{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %}
<div class="mt-8 ratings-reviews-section">
<!-- Ratings Summary -->
<div class="mt-8 ratings-reviews-section" style="display: flex;">
<div class="col-md-4 order-md-1 mt-8" style="max-width: 300px;">
<h2 class="reviews-header">
{{ _("Customer Ratings") }}
</h2>
{% if reviews %}
{% set rating_title = frappe.utils.cstr(average_rating) + " " + _("out of 5") %}
{{ ratings_with_title(average_whole_rating, rating_title, "lg", "rating-summary-title") }}
{% endif %}
<!-- Rating Progress Bars -->
<div class="rating-progress-bar-section">
{% for percent in reviews_per_rating %}
<div class="mt-4 col-sm-4 small rating-bar-title">
{{ loop.index }} star
</div>
<div class="row">
<div class="col-md-7">
<div class="progress rating-progress-bar" title="{{ percent }} % of reviews are {{ loop.index }} star">
<div class="progress-bar" role="progressbar"
aria-valuenow="{{ percent }}"
aria-valuemin="0" aria-valuemax="100"
style="width: {{ percent }}%; background-color: var(--text-on-green);">
</div>
</div>
</div>
<div class="col-sm-1 small">
{{ percent }}%
</div>
</div>
{% endfor %}
</div>
{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating) }}
<!-- Write a Review for legitimate users -->
{% if frappe.session.user != "Guest" %}
<button class="btn btn-light btn-write-review mr-2 mt-4 mb-4 w-100">
<button class="btn btn-light btn-write-review mr-2 mt-4 mb-4 w-100"
data-web-item="{{ doc.name }}">
{{ _("Write a Review") }}
</button>
{% endif %}
</div>
<!-- Reviews and Comments -->
@ -50,28 +19,14 @@
{{ _("Reviews") }}
</h2>
{% if reviews %}
{% for review in reviews %}
<!-- User review -->
<div class="mb-3 review">
{{ ratings_with_title(review.rating, _(review.review_title), "md", "user-review-title") }}
<div class="review-signature">
<span class="reviewer">{{ _(review.customer) }}</span>
<span>{{ review.published_on }}</span>
</div>
<div class="product-description mb-4 mt-4">
<p>
{{ _(review.comment) }}
</p>
</div>
</div>
{% endfor %}
{{ user_review(reviews) }}
{% if total_reviews > 4 %}
<div class="mt-6 mb-6"style="color: var(--primary);">
<a href="/reviews">{{ _("View all reviews") }}</a>
<a href="/customer_reviews?item_code={{ doc.item_code }}">{{ _("View all reviews") }}</a>
</div>
{% endif %}
{% else %}
<h6 class="text-muted mt-6">
{{ _("No Reviews") }}
@ -96,7 +51,6 @@
],
primary_action: function() {
var data = d.get_values();
$btn.prop('hidden', true);
frappe.call({
method: "erpnext.e_commerce.doctype.item_review.item_review.add_item_review",
args: {
@ -115,6 +69,7 @@
indicator: "green"
});
d.hide();
location.reload();
}
}
});

View File

@ -236,3 +236,59 @@
</p>
</div>
{%- endmacro -%}
{%- macro ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating)-%}
<!-- Ratings Summary -->
<h2 class="reviews-header">
{{ _("Customer Ratings") }}
</h2>
{% if reviews %}
{% set rating_title = frappe.utils.cstr(average_rating) + " " + _("out of 5") %}
{{ ratings_with_title(average_whole_rating, rating_title, "lg", "rating-summary-title") }}
{% endif %}
<!-- Rating Progress Bars -->
<div class="rating-progress-bar-section">
{% for percent in reviews_per_rating %}
<div class="mt-4 col-sm-4 small rating-bar-title">
{{ loop.index }} star
</div>
<div class="row">
<div class="col-md-7">
<div class="progress rating-progress-bar" title="{{ percent }} % of reviews are {{ loop.index }} star">
<div class="progress-bar" role="progressbar"
aria-valuenow="{{ percent }}"
aria-valuemin="0" aria-valuemax="100"
style="width: {{ percent }}%; background-color: var(--text-on-green);">
</div>
</div>
</div>
<div class="col-sm-1 small">
{{ percent }}%
</div>
</div>
{% endfor %}
</div>
{%- endmacro -%}
{%- macro user_review(reviews)-%}
<!-- User Reviews -->
<div class="user-reviews">
{% for review in reviews %}
<div class="mb-3 review">
{{ ratings_with_title(review.rating, _(review.review_title), "md", "user-review-title") }}
<div class="review-signature">
<span class="reviewer">{{ _(review.customer) }}</span>
<span>{{ review.published_on }}</span>
</div>
<div class="product-description mb-4 mt-4">
<p>
{{ _(review.comment) }}
</p>
</div>
</div>
{% endfor %}
</div>
{%- endmacro -%}

View File

@ -0,0 +1,45 @@
{% extends "templates/web.html" %}
{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %}
{% block title %} {{ _("Customer Reviews") }} {% endblock %}
{% block page_content %}
<div class="product-container col-md-12">
<div style="display: flex;">
<div class="col-md-4 order-md-1 mt-8" style="max-width: 300px;">
{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating) }}
<!-- Write a Review for legitimate users -->
{% if frappe.session.user != "Guest" %}
<button class="btn btn-light btn-write-review mr-2 mt-4 mb-4 w-100"
data-web-item="{{ web_item }}">
{{ _("Write a Review") }}
</button>
{% endif %}
</div>
<!-- Reviews and Comments -->
<div class="col-12 order-2 col-md-9 order-md-2 mt-8 ml-16">
<h2 class="reviews-header">
{{ _("Reviews") }}
</h2>
{% if reviews %}
{{ user_review(reviews) }}
{% if not reviews | len >= total_reviews %}
<button class="btn btn-light btn-view-more mr-2 mt-4 mb-4 w-30"
data-web-item="{{ web_item }}">
{{ _("View More") }}
</button>
{% endif %}
{% else %}
<h6 class="text-muted mt-6">
{{ _("No Reviews") }}
</h6>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,135 @@
$(() => {
class CustomerReviews {
constructor() {
this.bind_button_actions();
this.start = 0;
this.page_length = 10;
}
bind_button_actions() {
this.write_review();
this.view_more();
}
write_review() {
//TODO: make dialog popup on stray page
$('.page_content').on('click', '.btn-write-review', (e) => {
// Bind action on write a review button
const $btn = $(e.currentTarget);
let d = new frappe.ui.Dialog({
title: __("Write a Review"),
fields: [
{fieldname: "title", fieldtype: "Data", label: "Headline", reqd: 1},
{fieldname: "rating", fieldtype: "Rating", label: "Overall Rating", reqd: 1},
{fieldtype: "Section Break"},
{fieldname: "comment", fieldtype: "Small Text", label: "Your Review"}
],
primary_action: function() {
let me = this;
let data = d.get_values();
frappe.call({
method: "erpnext.e_commerce.doctype.item_review.item_review.add_item_review",
args: {
web_item: $btn.attr('data-web-item'),
title: data.title,
rating: data.rating,
comment: data.comment
},
freeze: true,
freeze_message: __("Submitting Review ..."),
callback: (r) => {
if(!r.exc) {
frappe.msgprint({
message: __("Thank you for submitting your review"),
title: __("Review Submitted"),
indicator: "green"
});
d.hide();
location.reload();
}
}
});
},
primary_action_label: __('Submit')
});
d.show();
});
}
view_more() {
$('.page_content').on('click', '.btn-view-more', (e) => {
// Bind action on view more button
const $btn = $(e.currentTarget);
$btn.prop('disabled', true);
this.start += this.page_length;
let me = this;
frappe.call({
method: "erpnext.e_commerce.doctype.item_review.item_review.get_item_reviews",
args: {
web_item: $btn.attr('data-web-item'),
start: me.start,
end: me.page_length
},
callback: (result) => {
if(result.message) {
let res = result.message;
me.get_user_review_html(res.reviews);
$btn.prop('disabled', false);
if (res.total_reviews <= (me.start + me.page_length)) {
$btn.hide();
}
}
}
})
});
}
get_user_review_html(reviews) {
let me = this;
let $content = $('.user-reviews');
reviews.forEach((review) => {
$content.append(`
<div class="mb-3 review">
<div style="display: flex;">
<div class="rating">
${me.get_review_stars(review.rating)}
</div>
<p class="ml-4 user-review-title">
<span>${__(review.review_title)}</span>
</p>
</div>
<div class="review-signature">
<span class="reviewer">${__(review.customer)}</span>
<span>${__(review.published_on)}</span>
</div>
<div class="product-description mb-4 mt-4">
<p>
${__(review.comment)}
</p>
</div>
</div>
`);
});
}
get_review_stars(rating) {
let stars = ``;
for(let i = 1; i < 6; i++) {
let fill_class = i <= rating ? 'star-click' : '';
stars += `<svg class="icon icon-md ${fill_class}">
<use href="#icon-star"></use>
</svg>`;
}
return stars;
}
}
new CustomerReviews();
});

View File

@ -0,0 +1,16 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
no_cache = 1
import frappe
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
def get_context(context):
context.full_page = True
context.reviews = None
if frappe.form_dict and frappe.form_dict.get("item_code"):
context.item_code = frappe.form_dict.get("item_code")
context.web_item = frappe.db.get_value("Website Item", {"item_code": context.item_code}, "name")
get_item_reviews(context.web_item, 0, 10, context)