[website] Support Portal (#14144)
* [init] support portal
* [support-portal] Get started sections and forum activity
* [support-portal] integrate API search for forums, docs, etc
* [support-portal] integrate doctype docs search via global search
* [support-portal] /help page UI
diff --git a/erpnext/templates/pages/help.html b/erpnext/templates/pages/help.html
new file mode 100644
index 0000000..f7568d5
--- /dev/null
+++ b/erpnext/templates/pages/help.html
@@ -0,0 +1,62 @@
+{% extends "templates/web.html" %}
+
+{% block title %} {{ _("Help") }} {% endblock %}
+
+{% block header %}<h1>{{ _("Help") }}</h1>
+
+<div style="margin-bottom: 20px;">
+ <form action='/search_help'>
+ <input name='q' class='form-control' type='text'
+ style='max-width: 400px; display: inline-block; margin-right: 10px;'
+ value='{{ frappe.form_dict.q or ''}}'
+ {% if not frappe.form_dict.q%}placeholder="{{ _("What do you need help with?") }}"{% endif %}>
+ <input type='submit'
+ class='btn btn-sm btn-primary btn-search' value="{{ _("Search") }}">
+ </form>
+</div>
+
+{% for section in get_started_sections %}
+<div style="margin-bottom: 30px;">
+ <h2>{{ section["name"] }}</h2>
+ {% for item in section["items"] %}
+ <div style="margin: 20px 0;">
+ <a href="{{ item.link }}"><b>{{ item.title }}</b></a>
+ {% if item.description -%}
+ <p>{{ item.description }}</p>
+ {%- endif %}
+ </div>
+ {% endfor %}
+ <p><a href="/kb/support">{{ _("See All Articles") }}</a></p>
+ <hr>
+</div>
+{% endfor %}
+
+<div style="margin-bottom: 30px;">
+ <h2>{{ _("Forum Activity") }}</h2>
+ {% for topic in topics %}
+ <div style="margin: 20px 0;">
+ <a href="{{ topic[post_params.link] }}">
+ <b>{{ topic[post_params.title] }}</b>
+ </a>
+ {% if topic[post_params.description] -%}
+ <p>{{ topic[post_params.description] }}</p>
+ {%- endif %}
+ </div>
+ {% endfor %}
+ <p><a href="{{ forum_url }}">{{ _("Visit the forums") }}</a></p>
+ <hr>
+</div>
+
+<div style="margin-bottom: 20px;">
+ <h2>{{ _("Your tickets") }}</h2>
+ {% for doc in issues %}
+ {% include "templates/includes/issue_row.html" %}
+ {% endfor %}
+ <p><a href="/issues">{{ _("See all open tickets") }}</a></p>
+</div>
+
+<a href="/issues?new=1" class="btn btn-primary btn-new btn-sm">
+ {{ _("Open a new ticket") }}
+</a>
+
+{% endblock %}
diff --git a/erpnext/templates/pages/help.py b/erpnext/templates/pages/help.py
new file mode 100644
index 0000000..754a09c
--- /dev/null
+++ b/erpnext/templates/pages/help.py
@@ -0,0 +1,41 @@
+from __future__ import unicode_literals
+import frappe, json
+
+import requests
+
+def get_context(context):
+ context.no_cache = 1
+ settings = frappe.get_doc("Support Settings", "Support Settings")
+ s = settings
+
+ # Get Started sections
+ sections = json.loads(s.get_started_sections)
+ context.get_started_sections = sections
+
+ # Forum posts
+ topics_data, post_params = get_forum_posts(s)
+ context.post_params = post_params
+ context.forum_url = s.forum_url
+ context.topics = topics_data[:3]
+
+ # Issues
+ context.issues = frappe.get_list("Issue")[:3]
+
+def get_forum_posts(s):
+ response = requests.get(s.forum_url + '/' + s.get_latest_query)
+ response.raise_for_status()
+ response_json = response.json()
+
+ topics_data = {} # it will actually be an array
+ key_list = s.response_key_list.split(',')
+ for key in key_list:
+ topics_data = response_json.get(key) if not topics_data else topics_data.get(key)
+
+ for topic in topics_data:
+ topic["link"] = s.forum_url + '/' + s.post_route_string + '/' + str(topic.get(s.post_route_key))
+
+ post_params = {
+ "title": s.post_title_key,
+ "description": s.post_description_key
+ }
+ return topics_data, post_params
diff --git a/erpnext/templates/pages/search_help.html b/erpnext/templates/pages/search_help.html
new file mode 100644
index 0000000..bf19540
--- /dev/null
+++ b/erpnext/templates/pages/search_help.html
@@ -0,0 +1,7 @@
+{% extends "templates/web.html" %}
+
+{% block page_content %}
+
+{% include "templates/includes/search_template.html" %}
+
+{% endblock %}
diff --git a/erpnext/templates/pages/search_help.py b/erpnext/templates/pages/search_help.py
new file mode 100644
index 0000000..e564e21
--- /dev/null
+++ b/erpnext/templates/pages/search_help.py
@@ -0,0 +1,99 @@
+from __future__ import unicode_literals
+import frappe, requests
+from frappe import _
+from jinja2 import utils
+from html2text import html2text
+from frappe.utils import sanitize_html
+from frappe.utils.global_search import search
+
+def get_context(context):
+ context.no_cache = 1
+ if frappe.form_dict.q:
+ query = str(utils.escape(sanitize_html(frappe.form_dict.q)))
+ context.title = _('Help Results for "{0}"').format(query)
+ context.route = '/search_help'
+ d = frappe._dict()
+ d.results_sections = get_help_results_sections(query)
+ context.update(d)
+ else:
+ context.title = _('Docs Search')
+
+@frappe.whitelist(allow_guest = True)
+def get_help_results_sections(text):
+ out = []
+ settings = frappe.get_doc("Support Settings", "Support Settings")
+
+ for api in settings.search_apis:
+ results = []
+ if api.source_type == "API":
+ response_json = get_response(api, text)
+ topics_data = get_topics_data(api, response_json)
+ results = prepare_api_results(api, topics_data)
+ else:
+ # Source type is Doctype
+ doctype = api.source_doctype
+ raw = search(text, 0, 20, doctype)
+ results = prepare_doctype_results(api, raw)
+
+ if results:
+ # Add section
+ out.append({
+ "title": api.source_name,
+ "results": results
+ })
+
+ return out
+
+def get_response(api, text):
+ response = requests.get(api.base_url + '/' + api.query_route, data={
+ api.search_term_param_name: text
+ })
+
+ response.raise_for_status()
+ return response.json()
+
+def get_topics_data(api, response_json):
+ if not response_json:
+ response_json = {}
+ topics_data = {} # it will actually be an array
+ key_list = api.response_result_key_path.split(',')
+
+ for key in key_list:
+ topics_data = response_json.get(key) if not topics_data else topics_data.get(key)
+
+ return topics_data or []
+
+def prepare_api_results(api, topics_data):
+ if not topics_data:
+ topics_data = []
+
+ results = []
+ for topic in topics_data:
+ route = api.base_url + '/' + (api.post_route + '/' if api.post_route else "")
+ for key in api.post_route_key_list.split(','):
+ route += unicode(topic[key])
+
+ results.append(frappe._dict({
+ 'title': topic[api.post_title_key],
+ 'preview': html2text(topic[api.post_description_key]),
+ 'route': route
+ }))
+ return results[:5]
+
+def prepare_doctype_results(api, raw):
+ results = []
+ for r in raw:
+ prepared_result = {}
+ parts = r["content"].split(' ||| ')
+
+ for part in parts:
+ pair = part.split(' : ', 1)
+ prepared_result[pair[0]] = pair[1]
+
+ results.append(frappe._dict({
+ 'title': prepared_result[api.result_title_field],
+ 'preview': prepared_result[api.result_preview_field],
+ 'route': prepared_result[api.result_route_field]
+ }))
+
+ return results