From e422e169538a4af1a76fddb036e256d07aa8f610 Mon Sep 17 00:00:00 2001 From: Makarand Bauskar Date: Thu, 2 Feb 2017 15:15:23 +0530 Subject: [PATCH] [WIP] Feedback Triggers (#2643) * [wip] feedback * [WIP] Feedback Trigger, Feedback Request, Feedback & Rating Timeline and Form Sidebar * [minor] minor fixes in feedback trigger * [Test Cases] Test Cases for Feedback Trigger * [minor] replaced sql_list by get_all changed the button label * [minor] removed whitespaces, fixed spelling mistakes --- .../doctype/communication/communication.js | 62 ++- .../doctype/communication/communication.json | 116 ++++- .../core/doctype/feedback_request/__init__.py | 0 .../feedback_request/feedback_request.js | 8 + .../feedback_request/feedback_request.json | 312 ++++++++++++ .../feedback_request/feedback_request.py | 28 ++ .../feedback_request/test_feedback_request.py | 12 + .../core/doctype/feedback_trigger/__init__.py | 0 .../feedback_trigger/feedback_trigger.js | 50 ++ .../feedback_trigger/feedback_trigger.json | 455 ++++++++++++++++++ .../feedback_trigger/feedback_trigger.py | 109 +++++ .../feedback_trigger/test_feedback_trigger.py | 102 ++++ frappe/desk/form/load.py | 22 +- .../email/doctype/email_alert/email_alert.py | 3 +- frappe/hooks.py | 6 +- frappe/public/build.json | 6 +- .../public/js/frappe/form/footer/timeline.js | 104 ++-- .../js/frappe/form/footer/timeline_item.html | 52 +- frappe/public/js/frappe/form/grid.js | 43 +- frappe/public/js/frappe/form/sidebar.js | 13 + .../frappe/form/templates/form_sidebar.html | 8 +- .../public/js/frappe/misc/rating_icons.html | 6 + frappe/public/js/frappe/misc/ratings.html | 8 + frappe/utils/jinja.py | 2 +- frappe/www/feedback.html | 94 ++++ frappe/www/feedback.py | 28 ++ 26 files changed, 1547 insertions(+), 102 deletions(-) create mode 100644 frappe/core/doctype/feedback_request/__init__.py create mode 100644 frappe/core/doctype/feedback_request/feedback_request.js create mode 100644 frappe/core/doctype/feedback_request/feedback_request.json create mode 100644 frappe/core/doctype/feedback_request/feedback_request.py create mode 100644 frappe/core/doctype/feedback_request/test_feedback_request.py create mode 100644 frappe/core/doctype/feedback_trigger/__init__.py create mode 100644 frappe/core/doctype/feedback_trigger/feedback_trigger.js create mode 100644 frappe/core/doctype/feedback_trigger/feedback_trigger.json create mode 100644 frappe/core/doctype/feedback_trigger/feedback_trigger.py create mode 100644 frappe/core/doctype/feedback_trigger/test_feedback_trigger.py create mode 100644 frappe/public/js/frappe/misc/rating_icons.html create mode 100644 frappe/public/js/frappe/misc/ratings.html create mode 100644 frappe/www/feedback.html create mode 100644 frappe/www/feedback.py diff --git a/frappe/core/doctype/communication/communication.js b/frappe/core/doctype/communication/communication.js index c3ede7d091..2272f7c357 100644 --- a/frappe/core/doctype/communication/communication.js +++ b/frappe/core/doctype/communication/communication.js @@ -31,6 +31,12 @@ frappe.ui.form.on("Communication", { } } + if(frm.doc.communication_type == "Feedback") { + frm.add_custom_button(__("Resend"), function() { + frm.events.resend_feedback(frm); + }); + } + if(frm.doc.status==="Open") { frm.add_custom_button(__("Close"), function() { frm.set_value("status", "Closed"); @@ -93,6 +99,60 @@ frappe.ui.form.on("Communication", { } }); d.show(); - } + }, + + resend_feedback: function(frm) { + /* resend the feedback request email */ + return frappe.call({ + method: "frappe.core.doctype.feedback_trigger.feedback_trigger.get_feedback_alert_details", + args: { + request: frm.doc.feedback_request, + reference_name: frm.doc.reference_name, + reference_doctype: frm.doc.reference_doctype + }, + callback: function(r) { + if(r.message) { + details = r.message; + + dialog = new frappe.ui.Dialog({ + title: __("Resend Feedback Request"), + fields: [ + { + "reqd": 1, + "label": __("Message"), + "fieldname": "message", + "fieldtype": "Text Editor", + "default": details.message + } + ], + }); + dialog.set_primary_action(__("Send"), function() { + args = dialog.get_values(); + if(!args) + return; + else + details.message = args.message + dialog.hide(); + + return frappe.call({ + method: "frappe.core.doctype.feedback_trigger.feedback_trigger.send_feedback_alert", + args: { + reference_name: frm.doc.reference_name, + reference_doctype: frm.doc.reference_doctype, + alert_details: details, + }, + freeze: true, + callback: function(r) { + frappe.msgprint(__("Feedback Alert for {0} is sent to {1}", + [frm.doc.reference_name, frm.doc.sender])); + } + }) + }); + + dialog.show(); + } + } + }); + } }); diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index a610127a6a..8592b48be2 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -400,7 +400,7 @@ "label": "Communication Type", "length": 0, "no_copy": 0, - "options": "Communication\nComment\nChat\nBot\nNotification", + "options": "Communication\nComment\nChat\nBot\nNotification\nFeedback", "permlevel": 0, "precision": "", "print_hide": 0, @@ -1208,6 +1208,118 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "feedback_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Feedback", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "feedback", + "fieldtype": "Text Editor", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Feedback", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "rating", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Rating", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "feedback_request", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Feedback Request", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, @@ -1221,7 +1333,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-01-20 05:20:58.187840", + "modified": "2017-01-27 16:11:37.344686", "modified_by": "Administrator", "module": "Core", "name": "Communication", diff --git a/frappe/core/doctype/feedback_request/__init__.py b/frappe/core/doctype/feedback_request/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/feedback_request/feedback_request.js b/frappe/core/doctype/feedback_request/feedback_request.js new file mode 100644 index 0000000000..f46e4c1e4d --- /dev/null +++ b/frappe/core/doctype/feedback_request/feedback_request.js @@ -0,0 +1,8 @@ +// Copyright (c) 2016, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Feedback Request', { + refresh: function(frm) { + + } +}); diff --git a/frappe/core/doctype/feedback_request/feedback_request.json b/frappe/core/doctype/feedback_request/feedback_request.json new file mode 100644 index 0000000000..bd444c40d1 --- /dev/null +++ b/frappe/core/doctype/feedback_request/feedback_request.json @@ -0,0 +1,312 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "field:key", + "beta": 0, + "creation": "2017-01-27 15:43:33.780808", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "is_sent", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Is Sent", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "is_feedback_submitted", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Feedback Submitted", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "key", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reference", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_doctype", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Reference DocType", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Reference Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_5", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "feedback_trigger", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Feedback Trigger", + "length": 0, + "no_copy": 0, + "options": "Feedback Trigger", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-01-30 16:37:34.346362", + "modified_by": "Administrator", + "module": "Core", + "name": "Feedback Request", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 0 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/feedback_request/feedback_request.py b/frappe/core/doctype/feedback_request/feedback_request.py new file mode 100644 index 0000000000..096d4f1884 --- /dev/null +++ b/frappe/core/doctype/feedback_request/feedback_request.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class FeedbackRequest(Document): + def before_insert(self): + from frappe.utils import random_string + + self.key = random_string(32) + +@frappe.whitelist(allow_guest=True) +def is_valid_feedback_request(key=None): + if not key: + return False + + is_feedback_submitted = frappe.db.get_value("Feedback Request", key, "is_feedback_submitted") + if is_feedback_submitted: + return False + else: + return True + +def delete_feedback_request(): + """ clear 100 days old feedback request """ + frappe.db.sql("""delete from `tabFeedback Request` where creation
{{ doc.name }} Delivered
", + "fieldname": "subject", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subject", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Optional: The alert will be sent if this expression is true", + "fieldname": "condition", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Condition", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_7", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "html_8", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "options": "

Condition Examples:

\n
doc.status==\"Closed\"\ndoc.due_date==nowdate()\ndoc.total > 40000\n
", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Message", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "message", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Message", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "example", + "fieldtype": "HTML", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Example", + "length": 0, + "no_copy": 0, + "options": "
Message Example
\n\n
<h3>Issue Resolved</h3>\n\n<p>Issue {{ doc.name }} Is resolved. Please check and confirm the same.</p>\n\n<p> Your Feedback is important for us. Please give us your Feedback for {{ doc.name }}</p>\n\n<p> Please visit the following url for feedback.</p>\n\n{{ feedback_url }}\n\n<h4>Details</h4>
", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-01-29 10:45:11.175365", + "modified_by": "Administrator", + "module": "Core", + "name": "Feedback Trigger", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "document_type", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/feedback_trigger/feedback_trigger.py b/frappe/core/doctype/feedback_trigger/feedback_trigger.py new file mode 100644 index 0000000000..7ca1ca6e80 --- /dev/null +++ b/frappe/core/doctype/feedback_trigger/feedback_trigger.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import json +import frappe +from frappe import _ +from frappe.utils import get_url +from frappe.model.document import Document +from frappe.utils.jinja import validate_template + +class FeedbackTrigger(Document): + def validate(self): + validate_template(self.subject) + validate_template(self.message) + self.validate_condition() + + def validate_condition(self): + temp_doc = frappe.new_doc(self.document_type) + if self.condition: + try: + eval(self.condition, get_context(temp_doc)) + except: + frappe.throw(_("The Condition '{0}' is invalid").format(self.condition)) + +@frappe.whitelist() +def send_feedback_alert(reference_doctype, reference_name, trigger=None, alert_details=None): + """ send feedback alert """ + details = json.loads(alert_details) if alert_details else \ + get_feedback_alert_details(reference_doctype, reference_name, trigger=trigger) + + if details: + feedback_request = details.pop("feedback_request") + frappe.sendmail(**details) + frappe.db.set_value("Feedback Request", feedback_request, "is_sent", 1) + +def trigger_feedback_alert(doc, method): + """ trigger the feedback alert""" + + feedback_trigger = frappe.db.get_value("Feedback Trigger", { "enabled": 1, "document_type": doc.doctype }) + if feedback_trigger: + frappe.enqueue('frappe.core.doctype.feedback_trigger.feedback_trigger.send_feedback_alert', + trigger=feedback_trigger, reference_doctype=doc.doctype, reference_name=doc.name, now=frappe.flags.in_test) + +@frappe.whitelist() +def get_feedback_alert_details(reference_doctype, reference_name, trigger=None, request=None): + feedback_url = "" + + if not trigger and not request: + frappe.throw("Can not find Feedback Alert for {0}".format(reference_name)) + elif not trigger and request: + trigger = frappe.db.get_value("Feedback Request", request, "feedback_trigger") + + # check if feedback mail alert is already sent but feedback is not submitted + # to avoid sending multiple feedback mail alerts + + feedback_requests = frappe.get_all("Feedback Request", { + "is_sent": 1, + "is_feedback_submitted": 0, + "reference_name": reference_name, + "reference_doctype": reference_doctype + }, ["name"]) + if feedback_requests: + frappe.throw(_("Feedback Alert Mail has been already sent to the recipient")) + + feedback_trigger = frappe.get_doc("Feedback Trigger", trigger) + doc = frappe.get_doc(reference_doctype, reference_name) + + context = get_context(doc) + + recipients = doc.get(feedback_trigger.email_fieldname, None) + if recipients and eval(feedback_trigger.condition, context): + subject = feedback_trigger.subject + feedback_request = frappe.get_doc({ + "doctype": "Feedback Request", + "reference_name": doc.name, + "reference_doctype": doc.doctype, + "feedback_trigger": feedback_trigger.name + }).insert(ignore_permissions=True) + + feedback_url = "{base_url}/feedback?reference_doctype={doctype}&reference_name={docname}&email={email_id}&key={nonce}".format( + base_url=get_url(), + doctype=doc.doctype, + docname=doc.name, + email_id=recipients, + nonce=feedback_request.name + ) + context.update({ "alert": feedback_trigger, "feedback_url": feedback_url }) + + if "{" in subject: + subject = frappe.render_template(feedback_trigger.subject, context) + + feedback_alert_message = frappe.render_template(feedback_trigger.message, context) + + return { + "subject": subject, + "recipients": recipients, + "reference_name":doc.name, + "reference_doctype":doc.doctype, + "message": feedback_alert_message, + "feedback_request": feedback_request.name + } + else: + return None + + +def get_context(doc): + return { "doc": doc } diff --git a/frappe/core/doctype/feedback_trigger/test_feedback_trigger.py b/frappe/core/doctype/feedback_trigger/test_feedback_trigger.py new file mode 100644 index 0000000000..bfe488aa80 --- /dev/null +++ b/frappe/core/doctype/feedback_trigger/test_feedback_trigger.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +# test_records = frappe.get_test_records('Feedback Trigger') +def get_feedback_request(todo, feedback_trigger): + return frappe.db.get_value("Feedback Request", { + "is_sent": 1, + "is_feedback_submitted": 0, + "reference_doctype": "ToDo", + "reference_name": todo, + "feedback_trigger": feedback_trigger + }) + +class TestFeedbackTrigger(unittest.TestCase): + def setUp(self): + new_user = frappe.get_doc(dict(doctype='User', email='test-feedback@example.com', + first_name='Tester')).insert(ignore_permissions=True) + new_user.add_roles("System Manager") + + def tearDown(self): + frappe.delete_doc("User", "test-feedback@example.com") + frappe.delete_doc("Feedback Trigger", "ToDo") + frappe.db.sql('delete from `tabEmail Queue`') + frappe.db.sql('delete from `tabFeedback Request`') + + def test_feedback_trigger(self): + """ Test feedback trigger """ + from frappe.www.feedback import accept + + frappe.delete_doc("Feedback Trigger", "ToDo") + frappe.db.sql('delete from `tabEmail Queue`') + frappe.db.sql('delete from `tabFeedback Request`') + + feedback_trigger = frappe.get_doc({ + "enabled": 1, + "doctype": "Feedback Trigger", + "document_type": "ToDo", + "email_field": "assigned_by", + "subject": "{{ doc.name }} Task Completed", + "condition": "doc.status == 'Closed'", + "message": """Task {{ doc.name }} is Completed by {{ doc.owner }}. +
Please visit the {{ feedback_url }} and give your feedback + regarding the Task {{ doc.name }}""" + }).insert(ignore_permissions=True) + + # create a todo + todo = frappe.get_doc({ + "doctype": "ToDo", + "owner": "test-feedback@example.com", + "allocated_by": "test-feedback@example.com", + "description": "Unable To Submit Sales Order #SO-00001" + }).insert(ignore_permissions=True) + + email_queue = frappe.db.sql("""select name from `tabEmail Queue` where + reference_doctype='ToDo' and reference_name='{0}'""".format(todo.name)) + + # feedback alert mail should be sent only on 'Closed' status + self.assertFalse(email_queue) + + # check if feedback mail alert is triggered + todo.status = "Closed" + todo.save(ignore_permissions=True) + + email_queue = frappe.db.sql("""select name from `tabEmail Queue` where + reference_doctype='ToDo' and reference_name='{0}'""".format(todo.name)) + + self.assertTrue(email_queue) + frappe.db.sql('delete from `tabEmail Queue`') + + # test if feedback is submitted for the todo + feedback_request = get_feedback_request(todo.name, feedback_trigger.name) + self.assertTrue(feedback_request) + + # test if mail alerts are triggered multiple times for same document + self.assertRaises(Exception, todo.save, ignore_permissions=True) + + # Test if feedback is submitted sucessfully + result = accept(feedback_request, "test-feedback@example.com", "ToDo", todo.name, "Great Work !!", 4) + self.assertTrue(result) + + # test if feedback is saved in Communication + docname = frappe.db.get_value("Communication", { + "reference_doctype": "ToDo", + "reference_name": todo.name, + "communication_type": "Feedback", + "feedback_request": feedback_request + }) + + communication = frappe.get_doc("Communication", docname) + self.assertEqual(communication.rating, 4) + self.assertEqual(communication.feedback, "Great Work !!") + + # test if link expired after feedback submission + self.assertRaises(Exception, accept, key=feedback_request, sender="test-feedback@example.com", + reference_doctype="ToDo", reference_name=todo.name, feedback="Thank You !!", rating=4) + + frappe.delete_doc("ToDo", todo.name) \ No newline at end of file diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index eb4867f876..17ca561db1 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -97,7 +97,8 @@ def get_docinfo(doc=None, doctype=None, name=None): 'versions': get_versions(doc), "assignments": get_assignments(doc.doctype, doc.name), "permissions": get_doc_permissions(doc), - "shared": frappe.share.get_users(doc.doctype, doc.name) + "shared": frappe.share.get_users(doc.doctype, doc.name), + "rating": get_feedback_rating(doc.doctype, doc.name) } def get_user_permissions(meta): @@ -139,7 +140,6 @@ def _get_communications(doctype, name, start=0, limit=20): elif c.communication_type=="Comment" and c.comment_type=="Comment": c.content = frappe.utils.markdown(c.content) - return communications def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=None, @@ -152,9 +152,9 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= timeline_doctype, timeline_name, reference_doctype, reference_name, link_doctype, link_name, - "Communication" as doctype''' + rating, feedback, "Communication" as doctype''' - conditions = '''communication_type in ("Communication", "Comment") + conditions = '''communication_type in ("Communication", "Comment", "Feedback") and ( (reference_doctype=%(doctype)s and reference_name=%(name)s) or ( @@ -206,3 +206,17 @@ def get_badge_info(doctypes, filters): def run_onload(doc): doc.set("__onload", frappe._dict()) doc.run_method("onload") + +def get_feedback_rating(doctype, docname): + """ get and return the latest feedback rating if available """ + + rating= frappe.get_all("Communication", filters={ + "reference_doctype": doctype, + "reference_name": docname, + "communication_type": "Feedback" + }, fields=["rating"], order_by="creation desc", as_list=True) + + if not rating: + return 0 + else: + return rating[0][0] \ No newline at end of file diff --git a/frappe/email/doctype/email_alert/email_alert.py b/frappe/email/doctype/email_alert/email_alert.py index 8627d71239..e42a17ac36 100755 --- a/frappe/email/doctype/email_alert/email_alert.py +++ b/frappe/email/doctype/email_alert/email_alert.py @@ -210,5 +210,4 @@ def evaluate_alert(doc, alert, event): frappe.throw(_("Error while evaluating Email Alert {0}. Please fix your template.").format(alert)) def get_context(doc): - return {"doc": doc, "nowdate": nowdate} - + return {"doc": doc, "nowdate": nowdate} \ No newline at end of file diff --git a/frappe/hooks.py b/frappe/hooks.py index 47c06b623b..e3830fb783 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -113,7 +113,8 @@ doc_events = { "on_cancel": [ "frappe.desk.notifications.clear_doctype_notifications", ], - "on_trash": "frappe.desk.notifications.clear_doctype_notifications" + "on_trash": "frappe.desk.notifications.clear_doctype_notifications", + "on_change": "frappe.core.doctype.feedback_trigger.feedback_trigger.trigger_feedback_alert" }, "Email Group Member": { "validate": "frappe.email.doctype.email_group.email_group.restrict_email_group" @@ -145,7 +146,8 @@ scheduler_events = { "frappe.utils.scheduler.restrict_scheduler_events_if_dormant", "frappe.limits.update_space_usage", "frappe.email.doctype.auto_email_report.auto_email_report.send_daily", - "frappe.desk.page.backups.backups.delete_downloadable_backups" + "frappe.desk.page.backups.backups.delete_downloadable_backups", + "frappe.core.doctype.feedback_request.feedback_request.delete_feedback_request" ], "monthly": [ "frappe.email.doctype.auto_email_report.auto_email_report.send_monthly" diff --git a/frappe/public/build.json b/frappe/public/build.json index b430c3ac5b..fb7be5f5ab 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -19,7 +19,8 @@ "public/js/frappe/class.js", "public/js/lib/microtemplate.js", "public/js/frappe/query_string.js", - "website/js/website.js" + "website/js/website.js", + "public/js/frappe/misc/rating_icons.html" ], "js/editor.min.js": [ "public/js/lib/jquery/jquery.hotkeys.js", @@ -150,7 +151,8 @@ "public/js/frappe/desk.js", "public/js/frappe/query_string.js", - "public/js/frappe/ui/charts.js" + "public/js/frappe/ui/charts.js", + "public/js/frappe/misc/rating_icons.html" ], "js/d3.min.js": [ "public/js/lib/d3.min.js", diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 5ca9c50dc6..e182de61b1 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -98,7 +98,7 @@ frappe.ui.form.Timeline = Class.extend({ $.each(communications.sort(function(a, b) { return a.creation > b.creation ? -1 : 1 }), function(i, c) { - if(c.content) { + if(c.content || c.feedback) { c.frm = me.frm; me.render_timeline_item(c); } @@ -267,6 +267,11 @@ frappe.ui.form.Timeline = Class.extend({ c.original_content = c.content; c.content = frappe.utils.toggle_blockquote(c.content); + } else if (c.communication_type==="Feedback") { + c.content = frappe.utils.strip_original_content(c.feedback); + + c.original_content = c.feedback; + c.content = frappe.utils.toggle_blockquote(c.feedback); } if(!frappe.utils.is_html(c.content)) { @@ -307,57 +312,64 @@ frappe.ui.form.Timeline = Class.extend({ }, is_communication_or_comment: function(c) { - return c.communication_type==="Communication" || (c.communication_type==="Comment" && (c.comment_type==="Comment"||c.comment_type==="Relinked")); + return c.communication_type==="Communication" + || c.communication_type==="Feedback" + || (c.communication_type==="Comment" && (c.comment_type==="Comment"||c.comment_type==="Relinked")); }, set_icon_and_color: function(c) { - c.icon = { - "Email": "octicon octicon-mail", - "Chat": "octicon octicon-comment-discussion", - "Phone": "octicon octicon-device-mobile", - "SMS": "octicon octicon-comment", - "Created": "octicon octicon-plus", - "Submitted": "octicon octicon-lock", - "Cancelled": "octicon octicon-x", - "Assigned": "octicon octicon-person", - "Assignment Completed": "octicon octicon-check", - "Comment": "octicon octicon-comment-discussion", - "Workflow": "octicon octicon-git-branch", - "Label": "octicon octicon-tag", - "Attachment": "octicon octicon-cloud-upload", - "Attachment Removed": "octicon octicon-trashcan", - "Shared": "octicon octicon-eye", - "Unshared": "octicon octicon-circle-slash", - "Like": "octicon octicon-heart", - "Edit": "octicon octicon-pencil", - "Relinked": "octicon octicon-check" - }[c.comment_type || c.communication_medium] - - c.color = { - "Email": "#3498db", - "Chat": "#3498db", - "Phone": "#3498db", - "SMS": "#3498db", - "Created": "#1abc9c", - "Submitted": "#1abc9c", - "Cancelled": "#c0392b", - "Assigned": "#f39c12", - "Assignment Completed": "#16a085", - "Comment": "#f39c12", - "Workflow": "#2c3e50", - "Label": "#2c3e50", - "Attachment": "#7f8c8d", - "Attachment Removed": "#eee", - "Relinked": "#16a085" - }[c.comment_type || c.communication_medium]; - - c.icon_fg = { - "Attachment Removed": "#333", - }[c.comment_type || c.communication_medium] + if(c.communication_type == "Feedback"){ + c.icon = "octicon octicon-comment-discussion" + c.rating_icons = frappe.render_template("rating_icons", {rating: c.rating, show_label: true}) + c.color = "#f39c12" + } else { + c.icon = { + "Email": "octicon octicon-mail", + "Chat": "octicon octicon-comment-discussion", + "Phone": "octicon octicon-device-mobile", + "SMS": "octicon octicon-comment", + "Created": "octicon octicon-plus", + "Submitted": "octicon octicon-lock", + "Cancelled": "octicon octicon-x", + "Assigned": "octicon octicon-person", + "Assignment Completed": "octicon octicon-check", + "Comment": "octicon octicon-comment-discussion", + "Workflow": "octicon octicon-git-branch", + "Label": "octicon octicon-tag", + "Attachment": "octicon octicon-cloud-upload", + "Attachment Removed": "octicon octicon-trashcan", + "Shared": "octicon octicon-eye", + "Unshared": "octicon octicon-circle-slash", + "Like": "octicon octicon-heart", + "Edit": "octicon octicon-pencil", + "Relinked": "octicon octicon-check" + }[c.comment_type || c.communication_medium] + + c.color = { + "Email": "#3498db", + "Chat": "#3498db", + "Phone": "#3498db", + "SMS": "#3498db", + "Created": "#1abc9c", + "Submitted": "#1abc9c", + "Cancelled": "#c0392b", + "Assigned": "#f39c12", + "Assignment Completed": "#16a085", + "Comment": "#f39c12", + "Workflow": "#2c3e50", + "Label": "#2c3e50", + "Attachment": "#7f8c8d", + "Attachment Removed": "#eee", + "Relinked": "#16a085" + }[c.comment_type || c.communication_medium]; + + c.icon_fg = { + "Attachment Removed": "#333", + }[c.comment_type || c.communication_medium] + } if(!c.icon_fg) c.icon_fg = "#fff"; - }, get_communications: function(with_versions) { var docinfo = this.frm.get_docinfo(), diff --git a/frappe/public/js/frappe/form/footer/timeline_item.html b/frappe/public/js/frappe/form/footer/timeline_item.html index 9cb34f7aaa..7ad4a2c9f2 100755 --- a/frappe/public/js/frappe/form/footer/timeline_item.html +++ b/frappe/public/js/frappe/form/footer/timeline_item.html @@ -23,6 +23,7 @@ {% if(data.communication_type==="Communication" + || data.communication_type==="Feedback" || (data.communication_type==="Comment" && data.comment_type==="Comment")) { %}
@@ -40,11 +41,11 @@ – {%= data.comment_on %} - {% if(data.communication_type==="Communication") { %} + {% if(inList(["Communication", "Feedback"], data.communication_type)) { %} {% if (frappe.model.can_read(\'Communication\')) { %} - {% } %} + {% } %} {% if (data.delivery_status) { if (in_list(["Sent", "Opened", "Clicked"], data.delivery_status)) { @@ -68,15 +69,15 @@ {% } %} {% } %} - {% if (frappe.model.can_read(\'Communication\')) { %} - - {% } %} + {% if (frappe.model.can_read(\'Communication\')) { %} + + {% } %} - {% if (data.communication_medium === "Email") { %} - {%= __("Reply") %} + {% if (data.communication_medium === "Email") { %} + {%= __("Reply") %} + {% } %} {% } %} - {% } %}