Merge branch 'develop' into snyk-fix-1b018faa364b5dc0e26502cba6099d54
This commit is contained in:
commit
abc2ec1496
228 changed files with 255479 additions and 256818 deletions
2
.github/workflows/translation_linter.yml
vendored
2
.github/workflows/translation_linter.yml
vendored
|
|
@ -18,5 +18,5 @@ jobs:
|
|||
- name: Validating Translation Syntax
|
||||
run: |
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
files=$(git diff --name-only $GITHUB_BASE_REF)
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
python $GITHUB_WORKSPACE/.github/frappe_linter/translation.py $files
|
||||
21
.travis.yml
21
.travis.yml
|
|
@ -1,5 +1,5 @@
|
|||
language: python
|
||||
dist: trusty
|
||||
dist: bionic
|
||||
|
||||
addons:
|
||||
hosts:
|
||||
|
|
@ -9,6 +9,10 @@ addons:
|
|||
postgresql: 9.5
|
||||
chrome: stable
|
||||
|
||||
services:
|
||||
- xvfb
|
||||
- mysql
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
|
|
@ -23,29 +27,24 @@ cache:
|
|||
|
||||
matrix:
|
||||
include:
|
||||
- name: "Python 3.6 MariaDB"
|
||||
python: 3.6
|
||||
- name: "Python 3.7 MariaDB"
|
||||
python: 3.7
|
||||
env: DB=mariadb TYPE=server
|
||||
script: bench --site test_site run-tests --coverage
|
||||
|
||||
- name: "Python 3.6 PostgreSQL"
|
||||
python: 3.6
|
||||
- name: "Python 3.7 PostgreSQL"
|
||||
python: 3.7
|
||||
env: DB=postgres TYPE=server
|
||||
script: bench --site test_site run-tests --coverage
|
||||
|
||||
- name: "Cypress"
|
||||
python: 3.6
|
||||
python: 3.7
|
||||
env: DB=mariadb TYPE=ui
|
||||
before_script:
|
||||
- bench --site test_site execute frappe.utils.install.complete_setup_wizard
|
||||
- bench --site test_site_producer execute frappe.utils.install.complete_setup_wizard
|
||||
script: bench --site test_site run-ui-tests frappe --headless
|
||||
|
||||
- name: "Python 2.7 MariaDB"
|
||||
python: 2.7
|
||||
env: DB=mariadb TYPE=server
|
||||
script: bench --site test_site run-tests --coverage
|
||||
|
||||
before_install:
|
||||
# install wkhtmltopdf
|
||||
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
|
||||
|
|
|
|||
25
CODEOWNERS
25
CODEOWNERS
|
|
@ -3,16 +3,17 @@
|
|||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
* @frappe/frappe-review-team
|
||||
website/ @scmmishra
|
||||
web_form/ @scmmishra
|
||||
templates/ @scmmishra
|
||||
www/ @scmmishra
|
||||
integrations/ @Mangesh-Khairnar
|
||||
patches/ @sahil28297
|
||||
dashboard/ @prssanna
|
||||
email/ @Thunderbottom
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
* @frappe/frappe-review-team
|
||||
website/ @scmmishra
|
||||
web_form/ @scmmishra
|
||||
templates/ @scmmishra
|
||||
www/ @scmmishra
|
||||
integrations/ @Mangesh-Khairnar
|
||||
patches/ @sahil28297
|
||||
dashboard/ @prssanna
|
||||
email/ @Thunderbottom
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
requirements.txt @gavindsouza
|
||||
commands/ @gavindsouza
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ if PY2:
|
|||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
__version__ = '12.0.0-dev'
|
||||
__version__ = '13.0.0-dev'
|
||||
__title__ = "Frappe Framework"
|
||||
|
||||
local = Local()
|
||||
|
|
@ -48,8 +48,12 @@ class _dict(dict):
|
|||
def copy(self):
|
||||
return _dict(dict(self).copy())
|
||||
|
||||
def _(msg, lang=None):
|
||||
"""Returns translated string in current lang, if exists."""
|
||||
def _(msg, lang=None, context=None):
|
||||
"""Returns translated string in current lang, if exists.
|
||||
Usage:
|
||||
_('Change')
|
||||
_('Change', context='Coins')
|
||||
"""
|
||||
from frappe.translate import get_full_dict
|
||||
from frappe.utils import strip_html_tags, is_html
|
||||
|
||||
|
|
@ -59,7 +63,7 @@ def _(msg, lang=None):
|
|||
if not lang:
|
||||
lang = local.lang
|
||||
|
||||
non_translated_msg = msg
|
||||
non_translated_string = msg
|
||||
|
||||
if is_html(msg):
|
||||
msg = strip_html_tags(msg)
|
||||
|
|
@ -67,8 +71,16 @@ def _(msg, lang=None):
|
|||
# msg should always be unicode
|
||||
msg = as_unicode(msg).strip()
|
||||
|
||||
translated_string = ''
|
||||
if context:
|
||||
string_key = '{msg}:{context}'.format(msg=msg, context=context)
|
||||
translated_string = get_full_dict(lang).get(string_key)
|
||||
|
||||
if not translated_string:
|
||||
translated_string = get_full_dict(lang).get(msg)
|
||||
|
||||
# return lang_full_dict according to lang passed parameter
|
||||
return get_full_dict(lang).get(msg) or non_translated_msg
|
||||
return translated_string or non_translated_string
|
||||
|
||||
def as_unicode(text, encoding='utf-8'):
|
||||
'''Convert to unicode if required'''
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
|
|||
installing = touch_file(get_site_path('locks', 'installing.lock'))
|
||||
atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password)
|
||||
|
||||
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
|
||||
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
|
||||
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
|
||||
db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
|
||||
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
|
||||
|
|
@ -101,7 +101,7 @@ def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password):
|
|||
|
||||
if installing and os.path.exists(installing):
|
||||
if mariadb_root_password:
|
||||
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True)
|
||||
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True, no_backup=True)
|
||||
shutil.rmtree(site)
|
||||
|
||||
frappe.destroy()
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ class Contact(Document):
|
|||
def get_default_contact(doctype, name):
|
||||
'''Returns default contact for the given doctype, name'''
|
||||
out = frappe.db.sql('''select parent,
|
||||
(select is_primary_contact from tabContact c where c.name = dl.parent)
|
||||
IFNULL((select is_primary_contact from tabContact c where c.name = dl.parent), 0)
|
||||
as is_primary_contact
|
||||
from
|
||||
`tabDynamic Link` dl
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class Comment(Document):
|
|||
def validate(self):
|
||||
if not self.comment_email:
|
||||
self.comment_email = frappe.session.user
|
||||
self.content = frappe.utils.sanitize_html(self.content)
|
||||
|
||||
def on_update(self):
|
||||
update_comment_in_doc(self)
|
||||
|
|
|
|||
|
|
@ -37,13 +37,11 @@ frappe.ui.form.on("Communication", {
|
|||
|
||||
if(frm.doc.status==="Open") {
|
||||
frm.add_custom_button(__("Close"), function() {
|
||||
frm.set_value("status", "Closed");
|
||||
frm.save();
|
||||
frm.trigger('mark_as_closed_open');
|
||||
});
|
||||
} else if (frm.doc.status !== "Linked") {
|
||||
frm.add_custom_button(__("Reopen"), function() {
|
||||
frm.set_value("status", "Open");
|
||||
frm.save();
|
||||
frm.trigger('mark_as_closed_open');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -61,30 +59,34 @@ frappe.ui.form.on("Communication", {
|
|||
|
||||
frm.add_custom_button(__("Reply All"), function() {
|
||||
frm.trigger('reply_all');
|
||||
}, "Actions");
|
||||
}, __("Actions"));
|
||||
|
||||
frm.add_custom_button(__("Forward"), function() {
|
||||
frm.trigger('forward_mail');
|
||||
}, "Actions");
|
||||
}, __("Actions"));
|
||||
|
||||
frm.add_custom_button(__("Mark as {0}", [frm.doc.seen? "Unread": "Read"]), function() {
|
||||
frm.add_custom_button(frm.doc.seen ? __("Mark as Unread") : __("Mark as Read"), function() {
|
||||
frm.trigger('mark_as_read_unread');
|
||||
}, "Actions");
|
||||
}, __("Actions"));
|
||||
|
||||
frm.add_custom_button(__("Add Contact"), function() {
|
||||
frm.trigger('add_to_contact');
|
||||
}, "Actions");
|
||||
frm.add_custom_button(__("Move"), function() {
|
||||
frm.trigger('show_move_dialog');
|
||||
}, __("Actions"));
|
||||
|
||||
if(frm.doc.email_status != "Spam")
|
||||
frm.add_custom_button(__("Mark as Spam"), function() {
|
||||
frm.trigger('mark_as_spam');
|
||||
}, "Actions");
|
||||
}, __("Actions"));
|
||||
|
||||
if(frm.doc.email_status != "Trash") {
|
||||
frm.add_custom_button(__("Move To Trash"), function() {
|
||||
frm.trigger('move_to_trash');
|
||||
}, "Actions");
|
||||
}, __("Actions"));
|
||||
}
|
||||
|
||||
frm.add_custom_button(__("Contact"), function() {
|
||||
frm.trigger('add_to_contact');
|
||||
}, __('Create'));
|
||||
}
|
||||
|
||||
if(frm.doc.communication_type=="Communication"
|
||||
|
|
@ -93,7 +95,7 @@ frappe.ui.form.on("Communication", {
|
|||
|
||||
frm.add_custom_button(__("Add Contact"), function() {
|
||||
frm.trigger('add_to_contact');
|
||||
}, "Actions");
|
||||
}, __("Actions"));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -145,6 +147,43 @@ frappe.ui.form.on("Communication", {
|
|||
d.show();
|
||||
},
|
||||
|
||||
show_move_dialog: function(frm) {
|
||||
var d = new frappe.ui.Dialog ({
|
||||
title: __("Move"),
|
||||
fields: [{
|
||||
"fieldtype": "Link",
|
||||
"options": "Email Account",
|
||||
"label": __("Email Account"),
|
||||
"fieldname": "email_account",
|
||||
"reqd": 1,
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {
|
||||
"name": ["!=", frm.doc.email_account],
|
||||
"enable_incoming": ["=", 1]
|
||||
}
|
||||
};
|
||||
}
|
||||
}],
|
||||
primary_action_label: __("Move"),
|
||||
primary_action(values) {
|
||||
d.hide();
|
||||
frappe.call({
|
||||
method: "frappe.email.inbox.move_email",
|
||||
args: {
|
||||
communication: frm.doc.name,
|
||||
email_account: values.email_account
|
||||
},
|
||||
freeze: true,
|
||||
callback: function() {
|
||||
window.history.back();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
|
||||
mark_as_read_unread: function(frm) {
|
||||
var action = frm.doc.seen? "Unread": "Read";
|
||||
var flag = "(\\SEEN)";
|
||||
|
|
@ -156,7 +195,26 @@ frappe.ui.form.on("Communication", {
|
|||
'action': action,
|
||||
'flag': flag
|
||||
},
|
||||
freeze: true
|
||||
freeze: true,
|
||||
callback: function() {
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
mark_as_closed_open: function(frm) {
|
||||
var status = frm.doc.status == "Open" ? "Closed" : "Open";
|
||||
|
||||
return frappe.call({
|
||||
method: "frappe.email.inbox.mark_as_closed_open",
|
||||
args: {
|
||||
communication: frm.doc.name,
|
||||
status: status
|
||||
},
|
||||
freeze: true,
|
||||
callback: function() {
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@
|
|||
"label": "More Information"
|
||||
},
|
||||
{
|
||||
"bold": 0,
|
||||
"default": "Now",
|
||||
"fieldname": "communication_date",
|
||||
"fieldtype": "Datetime",
|
||||
|
|
@ -424,6 +425,15 @@
|
|||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export":1,
|
||||
"print":1,
|
||||
"read": 1,
|
||||
"role": "Inbox User"
|
||||
},
|
||||
{
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ frappe.listview_settings['Communication'] = {
|
|||
"sent_or_received","recipients", "subject",
|
||||
"communication_medium", "communication_type",
|
||||
"sender", "seen", "reference_doctype", "reference_name",
|
||||
"has_attachment"
|
||||
"has_attachment", "communication_date"
|
||||
],
|
||||
|
||||
filters: [["status", "=", "Open"]],
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"column_break_3",
|
||||
"time_zone",
|
||||
"is_first_startup",
|
||||
"enable_onboarding",
|
||||
"setup_complete",
|
||||
"date_and_number_format",
|
||||
"date_format",
|
||||
|
|
@ -375,6 +376,7 @@
|
|||
"label": "Hide footer in auto email reports"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "chat",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Chat"
|
||||
|
|
@ -414,12 +416,18 @@
|
|||
"fieldname": "logout_on_password_reset",
|
||||
"fieldtype": "Check",
|
||||
"label": "Logout All Sessions on Password Reset"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_onboarding",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Onboarding"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-16 14:50:40.914532",
|
||||
"modified": "2020-05-01 19:21:15.496065",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ def get_translation_data():
|
|||
def create_translation(key, val):
|
||||
translation = frappe.new_doc('Translation')
|
||||
translation.language = key
|
||||
translation.source_name = val[0]
|
||||
translation.target_name = val[1]
|
||||
translation.source_text = val[0]
|
||||
translation.translated_text = val[1]
|
||||
translation.save()
|
||||
return translation
|
||||
|
|
|
|||
|
|
@ -3,19 +3,7 @@
|
|||
|
||||
|
||||
frappe.ui.form.on('Translation', {
|
||||
refresh: function(frm) {
|
||||
if(frm.is_new() || !(["Saved", "Deleted"].includes(frm.doc.status))) return;
|
||||
frm.add_custom_button('Contribute', function() {
|
||||
frappe.call({
|
||||
method: 'frappe.core.doctype.translation.translation.contribute_translation',
|
||||
args: {
|
||||
"language": frm.doc.language,
|
||||
"contributor": frm.doc.owner,
|
||||
"source_name": frm.doc.source_name,
|
||||
"target_name": frm.doc.target_name,
|
||||
"doc_name": frm.doc.name
|
||||
}
|
||||
});
|
||||
}).addClass('btn-primary');
|
||||
refresh: function() {
|
||||
//
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"_comments": "[]",
|
||||
"_liked_by": "[]",
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "hash",
|
||||
"creation": "2016-02-17 12:21:16.175465",
|
||||
|
|
@ -6,20 +9,21 @@
|
|||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"contributed",
|
||||
"language",
|
||||
"section_break_4",
|
||||
"source_name",
|
||||
"source_text",
|
||||
"context",
|
||||
"column_break_6",
|
||||
"target_name",
|
||||
"translated_text",
|
||||
"section_break_6",
|
||||
"status",
|
||||
"contributed_translation_doctype_name"
|
||||
"contribution_status",
|
||||
"contribution_docname"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Language",
|
||||
"options": "Language",
|
||||
"search_index": 1
|
||||
|
|
@ -28,44 +32,58 @@
|
|||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "If your data is in HTML, please copy paste the exact HTML code with the tags.",
|
||||
"fieldname": "source_name",
|
||||
"fieldtype": "Code",
|
||||
"label": "Source Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "target_name",
|
||||
"fieldtype": "Code",
|
||||
"in_list_view": 1,
|
||||
"label": "Translated Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "Saved",
|
||||
"depends_on": "eval: !doc.__islocal",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Saved\nContributed\nVerified\nPR sent\nDeleted",
|
||||
"fieldname": "context",
|
||||
"fieldtype": "Data",
|
||||
"label": "Context",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "contributed_translation_doctype_name",
|
||||
"default": "0",
|
||||
"fieldname": "contributed",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Contributed",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "doc.contributed",
|
||||
"fieldname": "contribution_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Contribution Status",
|
||||
"options": "\nPending\nVerified\nRejected",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "contribution_docname",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Contributed Translation Doctype Name",
|
||||
"label": "Contribution Document Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "If your data is in HTML, please copy paste the exact HTML code with the tags.",
|
||||
"fieldname": "source_text",
|
||||
"fieldtype": "Code",
|
||||
"label": "Source Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "translated_text",
|
||||
"fieldtype": "Code",
|
||||
"in_list_view": 1,
|
||||
"label": "Translated Text"
|
||||
}
|
||||
],
|
||||
"modified": "2019-06-18 19:03:38.640990",
|
||||
"links": [],
|
||||
"modified": "2020-03-12 13:28:48.223409",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Translation",
|
||||
|
|
@ -86,6 +104,6 @@
|
|||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "source_name",
|
||||
"title_field": "source_text",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -5,57 +5,77 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.translate import clear_cache
|
||||
from frappe.utils import strip_html_tags, is_html
|
||||
from frappe.integrations.utils import make_post_request
|
||||
from frappe.translate import get_translator_url
|
||||
import json
|
||||
|
||||
class Translation(Document):
|
||||
def validate(self):
|
||||
if is_html(self.source_name):
|
||||
if is_html(self.source_text):
|
||||
self.remove_html_from_source()
|
||||
|
||||
def remove_html_from_source(self):
|
||||
self.source_name = strip_html_tags(self.source_name).strip()
|
||||
self.source_text = strip_html_tags(self.source_text).strip()
|
||||
|
||||
def on_update(self):
|
||||
clear_cache()
|
||||
clear_user_translation_cache(self.language)
|
||||
|
||||
def on_trash(self):
|
||||
clear_cache()
|
||||
clear_user_translation_cache(self.language)
|
||||
|
||||
def onload(self):
|
||||
if self.contributed_translation_doctype_name:
|
||||
data = {"data": json.dumps({
|
||||
"doc_name": self.contributed_translation_doctype_name
|
||||
})}
|
||||
try:
|
||||
response = make_post_request(url=frappe.get_hooks("translation_contribution_status")[0], data=data)
|
||||
except Exception:
|
||||
frappe.msgprint("Something went wrong. Please check error log for more details")
|
||||
if response.get("message").get("message") == "Contributed Translation has been deleted":
|
||||
self.status = "Deleted"
|
||||
self.contributed_translation_doctype_name = ""
|
||||
self.save()
|
||||
else:
|
||||
self.status = response.get("message").get("status")
|
||||
self.save()
|
||||
def contribute(self):
|
||||
pass
|
||||
|
||||
def get_contribution_status(self):
|
||||
pass
|
||||
|
||||
@frappe.whitelist()
|
||||
def contribute_translation(language, contributor, source_name, target_name, doc_name):
|
||||
data = {"data": json.dumps({
|
||||
"language": language,
|
||||
"contributor": contributor,
|
||||
"source_name": source_name,
|
||||
"target_name": target_name,
|
||||
"posting_date": frappe.utils.nowdate()
|
||||
})}
|
||||
try:
|
||||
response = make_post_request(url=frappe.get_hooks("translation_contribution_url")[0], data=data)
|
||||
except Exception:
|
||||
frappe.msgprint("Something went wrong while contributing translation. Please check error log for more details")
|
||||
if response.get("message").get("message") == "Already exists":
|
||||
frappe.msgprint("Translation already exists")
|
||||
elif response.get("message").get("message") == "Added to contribution list":
|
||||
frappe.set_value("Translation", doc_name, "contributed_translation_doctype_name", response.get("message").get("doc_name"))
|
||||
frappe.msgprint("Translation successfully contributed")
|
||||
def create_translations(translation_map, language):
|
||||
from frappe.frappeclient import FrappeClient
|
||||
|
||||
translation_map = json.loads(translation_map)
|
||||
translation_map_to_send = frappe._dict({})
|
||||
# first create / update local user translations
|
||||
for source_id, translation_dict in translation_map.items():
|
||||
translation_dict = frappe._dict(translation_dict)
|
||||
existing_doc_name = frappe.db.get_all('Translation', {
|
||||
'source_text': translation_dict.source_text,
|
||||
'context': translation_dict.context or '',
|
||||
'language': language,
|
||||
})
|
||||
translation_map_to_send[source_id] = translation_dict
|
||||
if existing_doc_name:
|
||||
frappe.db.set_value('Translation', existing_doc_name[0].name, {
|
||||
'translated_text': translation_dict.translated_text,
|
||||
'contributed': 1,
|
||||
'contribution_status': 'Pending'
|
||||
})
|
||||
translation_map_to_send[source_id].name = existing_doc_name[0].name
|
||||
else:
|
||||
doc = frappe.get_doc({
|
||||
'doctype': 'Translation',
|
||||
'source_text': translation_dict.source_text,
|
||||
'contributed': 1,
|
||||
'contribution_status': 'Pending',
|
||||
'translated_text': translation_dict.translated_text,
|
||||
'context': translation_dict.context,
|
||||
'language': language
|
||||
})
|
||||
doc.insert()
|
||||
translation_map_to_send[source_id].name = doc.name
|
||||
|
||||
params = {
|
||||
'language': language,
|
||||
'contributor_email': frappe.session.user,
|
||||
'contributor_name': frappe.utils.get_fullname(frappe.session.user),
|
||||
'translation_map': json.dumps(translation_map_to_send)
|
||||
}
|
||||
|
||||
translator = FrappeClient(get_translator_url())
|
||||
added_translations = translator.post_api('translator.api.add_translations', params=params)
|
||||
|
||||
for local_docname, remote_docname in added_translations.items():
|
||||
frappe.db.set_value('Translation', local_docname, 'contribution_docname', remote_docname)
|
||||
|
||||
def clear_user_translation_cache(lang):
|
||||
frappe.cache().hdel('lang_user_translations', lang)
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ class User(Document):
|
|||
'frappe.core.doctype.user.user.create_contact',
|
||||
user=self,
|
||||
ignore_mandatory=True,
|
||||
now=frappe.flags.in_test
|
||||
now=frappe.flags.in_test or frappe.flags.in_install
|
||||
)
|
||||
if self.name not in ('Administrator', 'Guest') and not self.user_image:
|
||||
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ class CustomizeForm(Document):
|
|||
|
||||
# load custom translation
|
||||
translation = self.get_name_translation()
|
||||
self.label = translation.target_name if translation else ''
|
||||
self.label = translation.translated_text if translation else ''
|
||||
|
||||
#If allow_auto_repeat is set, add auto_repeat custom field.
|
||||
if self.allow_auto_repeat:
|
||||
|
|
@ -131,16 +131,17 @@ class CustomizeForm(Document):
|
|||
|
||||
def get_name_translation(self):
|
||||
'''Get translation object if exists of current doctype name in the default language'''
|
||||
return frappe.get_value('Translation',
|
||||
{'source_name': self.doc_type, 'language': frappe.local.lang or 'en'},
|
||||
['name', 'target_name'], as_dict=True)
|
||||
return frappe.get_value('Translation', {
|
||||
'source_text': self.doc_type,
|
||||
'language': frappe.local.lang or 'en'
|
||||
}, ['name', 'translated_text'], as_dict=True)
|
||||
|
||||
def set_name_translation(self):
|
||||
'''Create, update custom translation for this doctype'''
|
||||
current = self.get_name_translation()
|
||||
if current:
|
||||
if self.label and current.target_name != self.label:
|
||||
frappe.db.set_value('Translation', current.name, 'target_name', self.label)
|
||||
if self.label and current.translated_text != self.label:
|
||||
frappe.db.set_value('Translation', current.name, 'translated_text', self.label)
|
||||
frappe.translate.clear_cache()
|
||||
else:
|
||||
# clear translation
|
||||
|
|
@ -149,8 +150,8 @@ class CustomizeForm(Document):
|
|||
else:
|
||||
if self.label:
|
||||
frappe.get_doc(dict(doctype='Translation',
|
||||
source_name=self.doc_type,
|
||||
target_name=self.label,
|
||||
source_text=self.doc_type,
|
||||
translated_text=self.label,
|
||||
language_code=frappe.local.lang or 'en')).insert()
|
||||
|
||||
def clear_existing_doc(self):
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ class Workspace:
|
|||
self.user = user
|
||||
self.allowed_pages = get_allowed_pages()
|
||||
self.allowed_reports = get_allowed_reports()
|
||||
self.onboarding_doc = self.get_onboarding_doc()
|
||||
self.onboarding = None
|
||||
|
||||
self.table_counts = get_table_with_counts()
|
||||
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
|
||||
|
|
@ -51,6 +53,31 @@ class Workspace:
|
|||
self.get_pages_to_extend()
|
||||
return frappe.get_doc("Desk Page", self.page_name)
|
||||
|
||||
def get_onboarding_doc(self):
|
||||
# Check if onboarding is enabled
|
||||
if not frappe.get_system_settings("enable_onboarding"):
|
||||
return None
|
||||
|
||||
if not self.doc.onboarding:
|
||||
return None
|
||||
|
||||
if frappe.db.get_value("Onboarding", self.doc.onboarding, "is_complete"):
|
||||
return None
|
||||
|
||||
doc = frappe.get_doc("Onboarding", self.doc.onboarding)
|
||||
|
||||
# Check if user is allowed
|
||||
allowed_roles = set(doc.get_allowed_roles())
|
||||
user_roles = set(self.user.get_roles())
|
||||
if not allowed_roles & user_roles:
|
||||
return None
|
||||
|
||||
# Check if already complete
|
||||
if doc.check_completion():
|
||||
return None
|
||||
|
||||
return doc
|
||||
|
||||
def get_pages_to_extend(self):
|
||||
pages = frappe.get_all("Desk Page", filters={
|
||||
"extends": self.page_name,
|
||||
|
|
@ -96,6 +123,16 @@ class Workspace:
|
|||
'items': self.get_shortcuts()
|
||||
}
|
||||
|
||||
if self.onboarding_doc:
|
||||
self.onboarding = {
|
||||
'label': _(self.onboarding_doc.title),
|
||||
'subtitle': _(self.onboarding_doc.subtitle),
|
||||
'success': _(self.onboarding_doc.success_message),
|
||||
'docs_url': self.onboarding_doc.documentation_url,
|
||||
'user_can_dismiss': self.onboarding_doc.user_can_dismiss,
|
||||
'items': self.get_onboarding_steps()
|
||||
}
|
||||
|
||||
def get_cards(self):
|
||||
cards = self.doc.cards + get_custom_reports_and_doctypes(self.doc.module)
|
||||
if len(self.extended_cards):
|
||||
|
|
@ -207,6 +244,16 @@ class Workspace:
|
|||
|
||||
return items
|
||||
|
||||
def get_onboarding_steps(self):
|
||||
steps = []
|
||||
for doc in self.onboarding_doc.get_steps():
|
||||
step = doc.as_dict().copy()
|
||||
step.label = _(doc.title)
|
||||
steps.append(step)
|
||||
|
||||
return steps
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def get_desktop_page(page):
|
||||
|
|
@ -226,6 +273,7 @@ def get_desktop_page(page):
|
|||
'charts': wspace.charts,
|
||||
'shortcuts': wspace.shortcuts,
|
||||
'cards': wspace.cards,
|
||||
'onboarding': wspace.onboarding,
|
||||
'allow_customization': not wspace.doc.disable_user_customization
|
||||
}
|
||||
|
||||
|
|
@ -360,8 +408,8 @@ def save_customization(page, config):
|
|||
"charts_label": original_page.charts_label,
|
||||
"cards_label": original_page.cards_label,
|
||||
"shortcuts_label": original_page.shortcuts_label,
|
||||
"icon": original_page.icon,
|
||||
"module": original_page.module,
|
||||
"onboarding": original_page.onboarding,
|
||||
"developer_mode_only": original_page.developer_mode_only,
|
||||
"category": original_page.category
|
||||
})
|
||||
|
|
@ -432,3 +480,16 @@ def prepare_widget(config, doctype, parentfield):
|
|||
|
||||
prepare_widget_list.append(doc)
|
||||
return prepare_widget_list
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_onboarding_step(name, field, value):
|
||||
"""Update status of onboaridng step
|
||||
|
||||
Args:
|
||||
name (string): Name of the doc
|
||||
field (string): field to be updated
|
||||
value: Value to be updated
|
||||
|
||||
"""
|
||||
frappe.db.set_value("Onboarding Step", name, field, value)
|
||||
|
|
|
|||
|
|
@ -224,15 +224,14 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "This chart will be public to all Users if this is set",
|
||||
"description": "This chart will be available to all Users if this is set",
|
||||
"fieldname": "is_public",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Public",
|
||||
"permlevel": 1
|
||||
"label": "Is Public"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-04-23 13:01:07.178866",
|
||||
"modified": "2020-05-01 15:22:59.119341",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard Chart",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import frappe
|
|||
from frappe import _
|
||||
import datetime
|
||||
import json
|
||||
from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan
|
||||
from frappe.utils.dashboard import cache_source, get_from_date_from_timespan
|
||||
from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime
|
||||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.boot import get_allowed_reports
|
||||
|
|
@ -27,7 +27,7 @@ def get_permission_query_conditions(user):
|
|||
return None
|
||||
|
||||
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
|
||||
allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()])
|
||||
allowed_reports = tuple([key if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()])
|
||||
|
||||
return '''
|
||||
`tabDashboard Chart`.`document_type` in {allowed_doctypes}
|
||||
|
|
@ -76,7 +76,7 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
|
|||
if to_date and len(to_date):
|
||||
to_date = get_datetime(to_date)
|
||||
else:
|
||||
to_date = chart.to_date
|
||||
to_date = get_datetime(chart.to_date)
|
||||
|
||||
timegrain = time_interval or chart.time_interval
|
||||
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or []
|
||||
|
|
@ -110,7 +110,8 @@ def create_dashboard_chart(args):
|
|||
|
||||
@frappe.whitelist()
|
||||
def create_report_chart(args):
|
||||
create_dashboard_chart()
|
||||
create_dashboard_chart(args)
|
||||
args = frappe.parse_json(args)
|
||||
if args.dashboard:
|
||||
add_chart_to_dashboard(json.dumps(args))
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
"category",
|
||||
"restrict_to_domain",
|
||||
"onboarding",
|
||||
"icon",
|
||||
"column_break_3",
|
||||
"extends_another_page",
|
||||
"is_standard",
|
||||
|
|
@ -58,12 +57,6 @@
|
|||
"label": "Shortcuts",
|
||||
"options": "Desk Shortcut"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.extends_another_page == 0",
|
||||
"fieldname": "onboarding",
|
||||
"fieldtype": "Data",
|
||||
"label": "Onboarding"
|
||||
},
|
||||
{
|
||||
"fieldname": "restrict_to_domain",
|
||||
"fieldtype": "Link",
|
||||
|
|
@ -80,12 +73,6 @@
|
|||
"label": "Module",
|
||||
"options": "Module Def"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.extends_another_page == 0",
|
||||
"fieldname": "icon",
|
||||
"fieldtype": "Data",
|
||||
"label": "Icon"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
|
|
@ -196,10 +183,16 @@
|
|||
"fieldtype": "Data",
|
||||
"label": "For User",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "onboarding",
|
||||
"fieldtype": "Link",
|
||||
"label": "Onboarding",
|
||||
"options": "Onboarding"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-03-26 12:35:41.981432",
|
||||
"modified": "2020-04-26 12:21:46.205079",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Desk Page",
|
||||
|
|
|
|||
|
|
@ -72,11 +72,10 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "This card will be public to all Users if this is set",
|
||||
"description": "This card will be available to all Users if this is set",
|
||||
"fieldname": "is_public",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Public",
|
||||
"permlevel": 1
|
||||
"label": "Is Public"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
|
|
@ -100,7 +99,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-04-25 17:31:34.204607",
|
||||
"modified": "2020-05-01 15:23:29.550243",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Number Card",
|
||||
|
|
|
|||
27
frappe/desk/doctype/onboarding/onboarding.js
Normal file
27
frappe/desk/doctype/onboarding/onboarding.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Onboarding", {
|
||||
refresh: function(frm) {
|
||||
frappe.boot.developer_mode &&
|
||||
frm.set_intro(
|
||||
__(
|
||||
"Saving this will export this document as well as the steps linked here as json."
|
||||
),
|
||||
true
|
||||
);
|
||||
if (!frappe.boot.developer_mode) {
|
||||
frm.trigger("disable_form");
|
||||
}
|
||||
},
|
||||
|
||||
disable_form: function(frm) {
|
||||
frm.set_read_only();
|
||||
frm.fields
|
||||
.filter((field) => field.has_input)
|
||||
.forEach((field) => {
|
||||
frm.set_df_property(field.df.fieldname, "read_only", "1");
|
||||
});
|
||||
frm.disable_save();
|
||||
},
|
||||
});
|
||||
124
frappe/desk/doctype/onboarding/onboarding.json
Normal file
124
frappe/desk/doctype/onboarding/onboarding.json
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "Prompt",
|
||||
"creation": "2020-04-24 13:58:14.948024",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"subtitle",
|
||||
"module",
|
||||
"allow_roles",
|
||||
"column_break_4",
|
||||
"success_message",
|
||||
"documentation_url",
|
||||
"user_can_dismiss",
|
||||
"is_complete",
|
||||
"section_break_6",
|
||||
"steps"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subtitle",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subtitle",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module",
|
||||
"options": "Module Def",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "success_message",
|
||||
"fieldtype": "Data",
|
||||
"label": "Success Message",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Allow users to dismiss onboarding temporarily for a day",
|
||||
"fieldname": "user_can_dismiss",
|
||||
"fieldtype": "Check",
|
||||
"label": "User Can Dismiss "
|
||||
},
|
||||
{
|
||||
"fieldname": "documentation_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Documentation URL",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_complete",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Complete",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "steps",
|
||||
"fieldtype": "Table",
|
||||
"label": "Steps",
|
||||
"options": "Onboarding Step Map",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"description": "System managers are allowed by default",
|
||||
"fieldname": "allow_roles",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Allow Roles",
|
||||
"options": "Onboarding Permission",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-05-01 19:37:21.492405",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Onboarding",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
43
frappe/desk/doctype/onboarding/onboarding.py
Normal file
43
frappe/desk/doctype/onboarding/onboarding.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.export_file import export_to_files
|
||||
|
||||
|
||||
class Onboarding(Document):
|
||||
def on_update(self):
|
||||
if frappe.conf.developer_mode:
|
||||
export_to_files(record_list=[['Onboarding', self.name]], record_module=self.module)
|
||||
|
||||
for step in self.steps:
|
||||
export_to_files(record_list=[['Onboarding Step', step.step]], record_module=self.module)
|
||||
|
||||
def get_steps(self):
|
||||
return [frappe.get_doc("Onboarding Step", step.step) for step in self.steps]
|
||||
|
||||
def get_allowed_roles(self):
|
||||
all_roles = [role.role for role in self.allow_roles]
|
||||
if "System Manager" not in all_roles:
|
||||
all_roles.append("System Manager")
|
||||
|
||||
return all_roles
|
||||
|
||||
def check_completion(self):
|
||||
if self.is_complete:
|
||||
return True
|
||||
|
||||
steps = self.get_steps()
|
||||
is_complete = [bool(step.is_complete or step.is_skipped) for step in steps]
|
||||
if all(is_complete):
|
||||
self.is_complete = True
|
||||
self.save()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def before_export(self, doc):
|
||||
doc.is_complete = 0
|
||||
|
|
@ -6,5 +6,5 @@ from __future__ import unicode_literals
|
|||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestCSSClass(unittest.TestCase):
|
||||
class TestOnboarding(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Web View', {
|
||||
frappe.ui.form.on('Onboarding Permission', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
|
|
@ -1,31 +1,28 @@
|
|||
{
|
||||
"creation": "2019-11-19 12:22:42.805741",
|
||||
"actions": [],
|
||||
"creation": "2020-04-30 18:27:48.255489",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"label",
|
||||
"video_id"
|
||||
"role"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "role",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Label"
|
||||
},
|
||||
{
|
||||
"fieldname": "video_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Video"
|
||||
"label": "Role",
|
||||
"options": "Role",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-11-19 13:39:57.716248",
|
||||
"links": [],
|
||||
"modified": "2020-04-30 18:28:40.423802",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Onboarding Slide Help Link",
|
||||
"name": "Onboarding Permission",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
|
|
@ -6,5 +6,6 @@ from __future__ import unicode_literals
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class CSSClass(Document):
|
||||
|
||||
class OnboardingPermission(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestOnboardingPermission(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Onboarding Slide', {
|
||||
refresh: function(frm) {
|
||||
frm.toggle_reqd('ref_doctype', (frm.doc.slide_type=='Create' || frm.doc.slide_type=='Settings'));
|
||||
frm.toggle_reqd('slide_module', (frm.doc.slide_type=='Information' || frm.doc.slide_type=='Continue'));
|
||||
},
|
||||
|
||||
ref_doctype: function(frm) {
|
||||
frm.set_query('ref_doctype', function() {
|
||||
if (frm.doc.slide_type === 'Create') {
|
||||
return {
|
||||
filters: {
|
||||
'issingle': 0,
|
||||
'istable': 0
|
||||
}
|
||||
};
|
||||
} else if (frm.doc.slide_type === 'Settings') {
|
||||
return {
|
||||
filters: {
|
||||
'issingle': 1,
|
||||
'istable': 0
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
//fetch mandatory fields automatically
|
||||
if (frm.doc.ref_doctype) {
|
||||
frappe.model.clear_table(frm.doc, 'slide_fields');
|
||||
let fields = frappe.meta.get_docfields(frm.doc.ref_doctype, null, {
|
||||
reqd: 1
|
||||
});
|
||||
$.each(fields, function(_i, data) {
|
||||
let row = frappe.model.add_child(frm.doc, 'Onboarding Slide', 'slide_fields');
|
||||
row.label = data.label;
|
||||
row.fieldtype = data.fieldtype;
|
||||
row.fieldname = data.fieldname;
|
||||
row.options = data.options;
|
||||
});
|
||||
refresh_field('slide_fields');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
{
|
||||
"autoname": "field:slide_title",
|
||||
"creation": "2019-11-13 14:39:56.834658",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"slide_title",
|
||||
"app",
|
||||
"slide_order",
|
||||
"column_break_4",
|
||||
"image_src",
|
||||
"slide_module",
|
||||
"description_section_break",
|
||||
"slide_desc",
|
||||
"action_section_break",
|
||||
"slide_type",
|
||||
"column_break_6",
|
||||
"max_count",
|
||||
"add_more_button",
|
||||
"section_break_18",
|
||||
"ref_doctype",
|
||||
"slide_fields",
|
||||
"section_break_10",
|
||||
"domains",
|
||||
"column_break_12",
|
||||
"help_links",
|
||||
"is_completed"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "slide_title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Slide Title",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "slide_desc",
|
||||
"fieldtype": "HTML Editor",
|
||||
"label": "Slide Description"
|
||||
},
|
||||
{
|
||||
"default": "3",
|
||||
"depends_on": "add_more_button",
|
||||
"description": "The amount of times you want to repeat the set of fields (eg: if you want 3 customers in the slide, set this field to 3. Only the first set of fields is shown as mandatory in the slide)",
|
||||
"fieldname": "max_count",
|
||||
"fieldtype": "Int",
|
||||
"label": "Max Count"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
|
||||
"fieldname": "add_more_button",
|
||||
"fieldtype": "Check",
|
||||
"label": "Add More Button"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
|
||||
"fieldname": "slide_fields",
|
||||
"fieldtype": "Table",
|
||||
"label": "Slide Fields",
|
||||
"options": "Onboarding Slide Field"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_10",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "Specify in what all domains should the slides show up. If nothing is specified the slide is shown in all domains by default.",
|
||||
"fieldname": "domains",
|
||||
"fieldtype": "Table",
|
||||
"label": "Domains",
|
||||
"options": "Has Domain"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Add a help video link just in case user has no idea about what to fill in the slide.",
|
||||
"fieldname": "help_links",
|
||||
"fieldtype": "Table",
|
||||
"label": "Help Links",
|
||||
"options": "Onboarding Slide Help Link"
|
||||
},
|
||||
{
|
||||
"fieldname": "action_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Action Settings"
|
||||
},
|
||||
{
|
||||
"description": "If Slide Type is Create or Settings there should be a 'create_onboarding_docs' method in the {ref_doctype}.py file bound to be executed after the slide is completed.",
|
||||
"fieldname": "slide_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Slide Type",
|
||||
"options": "Information\nCreate\nSettings\nContinue",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "app",
|
||||
"fieldtype": "Select",
|
||||
"label": "App",
|
||||
"options": "Frappe\nERPNext",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "image_src",
|
||||
"fieldtype": "Data",
|
||||
"label": "Slide Image Source"
|
||||
},
|
||||
{
|
||||
"fieldname": "description_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
|
||||
"fieldname": "ref_doctype",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Document Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Determines the order of the slide in the wizard. If the slide is not to be displayed, priority should be set to 0.",
|
||||
"fieldname": "slide_order",
|
||||
"fieldtype": "Int",
|
||||
"label": "Slide Order"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.slide_type=='Information' || doc.slide_type=='Continue'",
|
||||
"fieldname": "slide_module",
|
||||
"fieldtype": "Link",
|
||||
"label": "Module",
|
||||
"options": "Module Def"
|
||||
},
|
||||
{
|
||||
"collapsible_depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
|
||||
"fieldname": "section_break_18",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Fields"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_completed",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Is Completed",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"modified": "2019-12-04 10:50:43.528901",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Onboarding Slide",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.modules.export_file import export_to_files
|
||||
|
||||
class OnboardingSlide(Document):
|
||||
def validate(self):
|
||||
if self.slide_type == 'Continue' and frappe.db.exists('Onboarding Slide', {'slide_type': 'Continue', 'name': ('!=', self.name)}):
|
||||
frappe.throw(_('An Onboarding Slide of Slide Type Continue already exists.'))
|
||||
|
||||
if self.slide_order:
|
||||
same_order_slide = frappe.db.exists('Onboarding Slide', {'slide_order': self.slide_order, 'name': ('!=', self.name)})
|
||||
if same_order_slide:
|
||||
frappe.throw(_('An Onboarding Slide <b>{0}</b> with the same slide order already exists').format(same_order_slide))
|
||||
|
||||
def on_update(self):
|
||||
if self.ref_doctype:
|
||||
module = frappe.db.get_value('DocType', self.ref_doctype, 'module')
|
||||
else:
|
||||
module = self.slide_module
|
||||
export_to_files(record_list=[['Onboarding Slide', self.name]], record_module=module)
|
||||
|
||||
def get_onboarding_slides_as_list():
|
||||
slides = []
|
||||
slide_docs = frappe.db.get_all('Onboarding Slide',
|
||||
filters={'is_completed': 0},
|
||||
or_filters={'slide_order': ('!=', 0), 'slide_type': 'Continue'},
|
||||
order_by='slide_order')
|
||||
|
||||
# to check if continue slide is required
|
||||
first_slide = get_first_slide()
|
||||
|
||||
for entry in slide_docs:
|
||||
# using get_doc because child table fields are not fetched in get_all
|
||||
slide_doc = frappe.get_doc('Onboarding Slide', entry.name)
|
||||
if frappe.scrub(slide_doc.app) in frappe.get_installed_apps():
|
||||
slide = frappe._dict(
|
||||
slide_type=slide_doc.slide_type,
|
||||
title=slide_doc.slide_title,
|
||||
help=slide_doc.slide_desc,
|
||||
fields=slide_doc.slide_fields,
|
||||
help_links=get_help_links(slide_doc),
|
||||
add_more=slide_doc.add_more_button,
|
||||
max_count=slide_doc.max_count,
|
||||
image_src=get_slide_image(slide_doc),
|
||||
ref_doctype=slide_doc.ref_doctype,
|
||||
app=slide_doc.app
|
||||
)
|
||||
if slide.slide_type == 'Continue':
|
||||
if is_continue_slide_required(first_slide):
|
||||
slides.insert(0, slide)
|
||||
else:
|
||||
slides.append(slide)
|
||||
|
||||
return slides
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_onboarding_slides():
|
||||
slides = []
|
||||
slide_list = get_onboarding_slides_as_list()
|
||||
|
||||
active_domains = frappe.get_active_domains()
|
||||
for slide in slide_list:
|
||||
if not slide.domains or any(domain in active_domains for domain in slide.domains):
|
||||
slides.append(slide)
|
||||
return slides
|
||||
|
||||
def get_help_links(slide_doc):
|
||||
links=[]
|
||||
for link in slide_doc.help_links:
|
||||
links.append({
|
||||
'label': link.label,
|
||||
'video_id': link.video_id
|
||||
})
|
||||
return links
|
||||
|
||||
def get_slide_image(slide_doc):
|
||||
if slide_doc.image_src:
|
||||
return slide_doc.image_src
|
||||
return None
|
||||
|
||||
def is_continue_slide_required(first_slide):
|
||||
# check if first slide itself is not completed
|
||||
if not first_slide.is_completed:
|
||||
return False
|
||||
|
||||
# check if there is any active slide which is not completed
|
||||
return frappe.db.exists('Onboarding Slide', {
|
||||
'is_completed': 0,
|
||||
'slide_order': ('!=', 0),
|
||||
'slide_type': ('!=', 'Continue')
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_onboarding_docs(values, doctype=None, app=None, slide_type=None):
|
||||
data = json.loads(values)
|
||||
doc = frappe.new_doc(doctype)
|
||||
try:
|
||||
if hasattr(doc, 'create_onboarding_docs'):
|
||||
doc.flags.ignore_validate = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.create_onboarding_docs(data)
|
||||
else:
|
||||
create_generic_onboarding_doc(data, doctype, slide_type)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def create_generic_onboarding_doc(data, doctype, slide_type):
|
||||
if slide_type == 'Settings':
|
||||
doc = frappe.get_single(doctype)
|
||||
for entry in data:
|
||||
doc.set(entry, data.get(entry))
|
||||
doc.save()
|
||||
|
||||
elif slide_type == 'Create':
|
||||
doc = frappe.new_doc(doctype)
|
||||
for entry in data:
|
||||
doc.set(entry, data.get(entry))
|
||||
doc.flags.ignore_validate = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.insert()
|
||||
|
||||
@frappe.whitelist()
|
||||
def mark_slide_as_completed(slide_title):
|
||||
frappe.db.set_value('Onboarding Slide', slide_title, 'is_completed', 1)
|
||||
|
||||
def get_first_slide():
|
||||
slides = frappe.db.get_all('Onboarding Slide',
|
||||
filters={'slide_order': ('!=', 0), 'slide_type': ('!=', 'Continue')},
|
||||
order_by='slide_order',
|
||||
fields=['name', 'is_completed']
|
||||
)
|
||||
return slides[0]
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
{
|
||||
"creation": "2019-11-13 13:35:08.617909",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"label",
|
||||
"fieldtype",
|
||||
"fieldname",
|
||||
"align",
|
||||
"placeholder",
|
||||
"reqd",
|
||||
"column_break_4",
|
||||
"options"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Label"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldtype",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldtype",
|
||||
"options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nFloat\nHTML\nInt\nRating\nSelect\nLink\nSmall Text\nText\nText Editor\nSection Break\nColumn Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldname"
|
||||
},
|
||||
{
|
||||
"fieldname": "align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Align",
|
||||
"options": "\ncenter\nleft\nright"
|
||||
},
|
||||
{
|
||||
"fieldname": "placeholder",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Placeholder"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reqd",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mandatory"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "options",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Options"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-12-02 16:43:51.930018",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Onboarding Slide Field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, 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 OnboardingSlideHelpLink(Document):
|
||||
pass
|
||||
59
frappe/desk/doctype/onboarding_step/onboarding_step.js
Normal file
59
frappe/desk/doctype/onboarding_step/onboarding_step.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Onboarding Step", {
|
||||
refresh: function(frm) {
|
||||
frappe.boot.developer_mode &&
|
||||
frm.set_intro(
|
||||
__(
|
||||
"To export this step as JSON, link it in a Onboarding document and save the document."
|
||||
),
|
||||
true
|
||||
);
|
||||
if (frm.doc.reference_document && frm.doc.action == "Update Settings") {
|
||||
setup_fields(frm);
|
||||
}
|
||||
|
||||
if (!frappe.boot.developer_mode) {
|
||||
frm.trigger("disable_form");
|
||||
}
|
||||
},
|
||||
|
||||
reference_document: function(frm) {
|
||||
if (frm.doc.reference_document && frm.doc.action == "Update Settings") {
|
||||
setup_fields(frm);
|
||||
}
|
||||
},
|
||||
|
||||
disable_form: function(frm) {
|
||||
frm.set_read_only();
|
||||
frm.fields
|
||||
.filter((field) => field.has_input)
|
||||
.forEach((field) => {
|
||||
frm.set_df_property(field.df.fieldname, "read_only", "1");
|
||||
});
|
||||
frm.disable_save();
|
||||
},
|
||||
});
|
||||
|
||||
function setup_fields(frm) {
|
||||
if (frm.doc.reference_document && frm.doc.action == "Update Settings") {
|
||||
frappe.model.with_doctype(frm.doc.reference_document, () => {
|
||||
let fields = frappe
|
||||
.get_meta(frm.doc.reference_document)
|
||||
.fields.filter((df) => {
|
||||
return ["Data", "Check", "Int", "Link", "Select"].includes(
|
||||
df.fieldtype
|
||||
);
|
||||
})
|
||||
.map((df) => {
|
||||
return {
|
||||
label: `${__(df.label)} (${df.fieldname})`,
|
||||
value: df.fieldname,
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_df_property("field", "options", fields);
|
||||
});
|
||||
}
|
||||
}
|
||||
165
frappe/desk/doctype/onboarding_step/onboarding_step.json
Normal file
165
frappe/desk/doctype/onboarding_step/onboarding_step.json
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "prompt",
|
||||
"creation": "2020-04-14 15:50:25.782387",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"column_break_2",
|
||||
"is_mandatory",
|
||||
"is_complete",
|
||||
"is_skipped",
|
||||
"section_break_5",
|
||||
"action",
|
||||
"column_break_7",
|
||||
"reference_document",
|
||||
"reference_report",
|
||||
"report_reference_doctype",
|
||||
"report_type",
|
||||
"report_description",
|
||||
"field",
|
||||
"value_to_validate",
|
||||
"video_url"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_mandatory",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Mandatory"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_complete",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Complete"
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "action",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action",
|
||||
"options": "Create Entry\nUpdate Settings\nView Report\nWatch Video",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\"",
|
||||
"fieldname": "reference_document",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Document",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.action == \"View Report\"",
|
||||
"fieldname": "reference_report",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Report",
|
||||
"mandatory_depends_on": "eval:doc.action == \"View Report\"",
|
||||
"options": "Report"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.action == \"Watch Video\"",
|
||||
"fieldname": "video_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Video URL"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.action == \"View Report\"",
|
||||
"fetch_from": "reference_report.report_type",
|
||||
"fieldname": "report_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Report Type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_skipped",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Skipped"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.action == \"Update Settings\"",
|
||||
"fieldname": "field",
|
||||
"fieldtype": "Select",
|
||||
"label": "Field"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.action == \"Update Settings\"",
|
||||
"description": "Use % for any non empty value.",
|
||||
"fieldname": "value_to_validate",
|
||||
"fieldtype": "Data",
|
||||
"label": "Value to Validate"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.action == \"View Report\"",
|
||||
"description": "This will be shown to the user in a dialog after routing to the report",
|
||||
"fieldname": "report_description",
|
||||
"fieldtype": "Data",
|
||||
"label": "Report Description",
|
||||
"mandatory_depends_on": "eval:doc.action == \"View Report\""
|
||||
},
|
||||
{
|
||||
"fetch_from": "reference_report.ref_doctype",
|
||||
"fieldname": "report_reference_doctype",
|
||||
"fieldtype": "Data",
|
||||
"label": "Report Reference Doctype",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-05-04 12:53:19.276952",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Onboarding Step",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# Copyright (c) 2020, 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 OnboardingSlideField(Document):
|
||||
pass
|
||||
class OnboardingStep(Document):
|
||||
def before_export(self, doc):
|
||||
doc.is_complete = 0
|
||||
doc.is_skipped = 0
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestOnboardingSlide(unittest.TestCase):
|
||||
class TestOnboardingStep(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2020-04-28 22:06:08.544187",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"step"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "step",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Step",
|
||||
"options": "Onboarding Step",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-28 22:06:09.503406",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Onboarding Step Map",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -6,5 +6,6 @@ from __future__ import unicode_literals
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class WebViewComponent(Document):
|
||||
|
||||
class OnboardingStepMap(Document):
|
||||
pass
|
||||
|
|
@ -41,6 +41,11 @@ class Leaderboard {
|
|||
return field;
|
||||
});
|
||||
}
|
||||
|
||||
// For translation. Do not remove this
|
||||
// __("This Week"), __("This Month"), __("This Quarter"), __("This Year"),
|
||||
// __("Last Week"), __("Last Month"), __("Last Quarter"), __("Last Year"),
|
||||
// __("All Time"), __("Select From Date")
|
||||
this.timespans = [
|
||||
"This Week", "This Month", "This Quarter", "This Year",
|
||||
"Last Week", "Last Month", "Last Quarter", "Last Year",
|
||||
|
|
@ -107,7 +112,7 @@ class Leaderboard {
|
|||
this.timespans.map(d => {
|
||||
return {"label": __(d), value: d };
|
||||
})
|
||||
);
|
||||
);
|
||||
this.create_from_date_field();
|
||||
|
||||
this.type_select = this.page.add_select(__("Field"),
|
||||
|
|
@ -188,30 +193,12 @@ class Leaderboard {
|
|||
|
||||
this.$search_box =
|
||||
$(`<div class="leaderboard-search form-group col-md-3">
|
||||
<input type="text" placeholder="Search" class="form-control leaderboard-search-input input-sm">
|
||||
<input type="text" placeholder="Search" data-element="search" class="form-control leaderboard-search-input input-sm">
|
||||
</div>`);
|
||||
|
||||
$(this.parent).find(".page-form").append(this.$search_box);
|
||||
}
|
||||
|
||||
setup_search(list_items) {
|
||||
let $search_input = this.$search_box.find(".leaderboard-search-input");
|
||||
|
||||
this.$search_box.on("keyup", ()=> {
|
||||
let text_filter = $search_input.val().toLowerCase();
|
||||
text_filter = text_filter.replace(/^\s+|\s+$/g, '');
|
||||
for (var i = 0; i < list_items.length; i++) {
|
||||
let text = list_items.eq(i).find(".list-id").text().trim().toLowerCase();
|
||||
|
||||
if (text.includes(text_filter)) {
|
||||
list_items.eq(i).css("display", "");
|
||||
} else {
|
||||
list_items.eq(i).css("display", "none");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
show_leaderboard(doctype) {
|
||||
if (this.doctypes.length) {
|
||||
if (this.doctypes.includes(doctype)) {
|
||||
|
|
@ -273,7 +260,7 @@ class Leaderboard {
|
|||
if (res && res.message.length) {
|
||||
me.message = null;
|
||||
me.$container.find(".leaderboard-list").html(me.render_list_view(res.message));
|
||||
me.setup_search($(me.parent).find('.list-item-container'));
|
||||
frappe.utils.setup_search($(me.parent), ".list-item-container", ".list-id");
|
||||
} else {
|
||||
me.$graph_area.hide();
|
||||
me.message = __("No items found.");
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.doctype.global_search_settings.global_search_settings import update_global_search_doctypes
|
||||
from frappe.utils.dashboard import sync_dashboards
|
||||
|
||||
def install():
|
||||
update_genders()
|
||||
update_salutations()
|
||||
update_global_search_doctypes()
|
||||
setup_email_linking()
|
||||
sync_dashboards()
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_genders():
|
||||
|
|
@ -35,4 +37,3 @@ def setup_email_linking():
|
|||
"email_id": "email_linking@example.com",
|
||||
})
|
||||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
|
||||
|
|
@ -232,6 +232,9 @@ def disable_future_access():
|
|||
frappe.db.set_value('System Settings', 'System Settings', 'setup_complete', 1)
|
||||
frappe.db.set_value('System Settings', 'System Settings', 'is_first_startup', 1)
|
||||
|
||||
# Enable onboarding after install
|
||||
frappe.db.set_value('System Settings', 'System Settings', 'enable_onboarding', 1)
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
# remove all roles and add 'Administrator' to prevent future access
|
||||
page = frappe.get_doc('Page', 'setup-wizard')
|
||||
|
|
|
|||
35
frappe/desk/page/translation_tool/translation_tool.css
Normal file
35
frappe/desk/page/translation_tool/translation_tool.css
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
.translation-item {
|
||||
font-size: 12px;
|
||||
padding: 12px 15px;
|
||||
min-height: 40px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
.translation-item:hover {
|
||||
background-color: #fafbfc;
|
||||
}
|
||||
.translation-item.active {
|
||||
background-color: #fffce7;
|
||||
}
|
||||
|
||||
.translation-edit-section {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.translation-tool {
|
||||
border: 0px 1px 1px 1px solid #d1d8dd;
|
||||
width: 100%;
|
||||
height: 72vh;
|
||||
}
|
||||
|
||||
.left-side {
|
||||
padding: 0px;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.contributed-translation {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
20
frappe/desk/page/translation_tool/translation_tool.html
Normal file
20
frappe/desk/page/translation_tool/translation_tool.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<div class="translation-tool">
|
||||
<div class="col-sm-5 border-right left-side">
|
||||
<div class="level list-row list-row-head text-muted small">
|
||||
<div class="list-row-col ellipsis list-subject level">
|
||||
<span class="level-item">{%= __("Contributed Translations") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="translation-item-tr"></div>
|
||||
<div class="level list-row list-row-head text-muted small border-top">
|
||||
<div class="list-row-col ellipsis list-subject level">
|
||||
<span class="level-item">{%= __("Source Text") %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="translation-item-container"></div>
|
||||
</div>
|
||||
<div class="translation-edit-section col-sm-7">
|
||||
<div class="translation-edit-form"></div>
|
||||
<div class="other-contributions padding"></div>
|
||||
</div>
|
||||
</div>
|
||||
461
frappe/desk/page/translation_tool/translation_tool.js
Normal file
461
frappe/desk/page/translation_tool/translation_tool.js
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
frappe.pages['translation-tool'].on_page_load = function(wrapper) {
|
||||
var page = frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: 'Translation Tool',
|
||||
single_column: true
|
||||
});
|
||||
|
||||
frappe.translation_tool = new TranslationTool(page);
|
||||
};
|
||||
|
||||
class TranslationTool {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.wrapper = $(page.body);
|
||||
this.wrapper.append(frappe.render_template('translation_tool'));
|
||||
frappe.utils.bind_actions_with_object(this.wrapper, this);
|
||||
this.active_translation = null;
|
||||
this.edited_translations = {};
|
||||
this.setup_search_box();
|
||||
this.setup_language_filter();
|
||||
this.page.set_primary_action(
|
||||
__('Contribute Translations'),
|
||||
this.show_confirmation_dialog.bind(this)
|
||||
);
|
||||
this.page.set_secondary_action(
|
||||
__('Refresh'),
|
||||
this.fetch_messages_then_render.bind(this)
|
||||
);
|
||||
this.update_header();
|
||||
}
|
||||
|
||||
setup_language_filter() {
|
||||
let languages = Object.keys(frappe.boot.lang_dict).map(language_label => {
|
||||
let value = frappe.boot.lang_dict[language_label];
|
||||
return {
|
||||
label: `${language_label} (${value})`,
|
||||
value: value
|
||||
};
|
||||
});
|
||||
|
||||
let language_selector = this.page.add_field({
|
||||
fieldname: 'language',
|
||||
fieldtype: 'Select',
|
||||
options: languages,
|
||||
change: () => {
|
||||
let language = language_selector.get_value();
|
||||
localStorage.setItem('translation_language', language);
|
||||
this.language = language;
|
||||
this.fetch_messages_then_render();
|
||||
}
|
||||
});
|
||||
let translation_language = localStorage.getItem('translation_language');
|
||||
if (translation_language || frappe.boot.lang !== 'en') {
|
||||
language_selector.set_value(translation_language || frappe.boot.lang);
|
||||
} else {
|
||||
frappe.prompt(
|
||||
{
|
||||
label: __('Please select target language for translation'),
|
||||
fieldname: 'language',
|
||||
fieldtype: 'Select',
|
||||
options: languages,
|
||||
reqd: 1
|
||||
},
|
||||
values => {
|
||||
language_selector.set_value(values.language);
|
||||
},
|
||||
__('Select Language')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setup_search_box() {
|
||||
let search_box = this.page.add_field({
|
||||
fieldname: 'search',
|
||||
fieldtype: 'Data',
|
||||
label: __('Search Source Text'),
|
||||
change: () => {
|
||||
this.search_text = search_box.get_value();
|
||||
this.fetch_messages_then_render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetch_messages_then_render() {
|
||||
this.fetch_messages().then(messages => {
|
||||
this.messages = messages;
|
||||
this.render_messages(messages);
|
||||
});
|
||||
this.setup_local_contributions();
|
||||
}
|
||||
|
||||
fetch_messages() {
|
||||
frappe.dom.freeze(__('Fetching...'));
|
||||
return frappe
|
||||
.xcall('frappe.translate.get_messages', {
|
||||
language: this.language,
|
||||
search_text: this.search_text
|
||||
})
|
||||
.then(messages => {
|
||||
return messages;
|
||||
})
|
||||
.finally(() => {
|
||||
frappe.dom.unfreeze();
|
||||
});
|
||||
}
|
||||
|
||||
render_messages(messages) {
|
||||
let template = message => `
|
||||
<div
|
||||
class="translation-item"
|
||||
data-message-id="${encodeURIComponent(message.id)}"
|
||||
data-action="on_translation_click">
|
||||
<div class="bold ellipsis">
|
||||
<span class="indicator ${this.get_indicator_color(message)}">
|
||||
<span>${frappe.utils.escape_html(message.source_text)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let html = messages.map(template).join('');
|
||||
this.wrapper.find('.translation-item-container').html(html);
|
||||
}
|
||||
|
||||
on_translation_click(e, $el) {
|
||||
let message_id = decodeURIComponent($el.data('message-id'));
|
||||
this.wrapper.find('.translation-item').removeClass('active');
|
||||
$el.addClass('active');
|
||||
this.active_translation = this.messages.find(m => m.id === message_id);
|
||||
this.edit_translation(this.active_translation);
|
||||
}
|
||||
|
||||
edit_translation(translation) {
|
||||
if (this.form) {
|
||||
this.form.set_values({});
|
||||
}
|
||||
this.get_additional_info(translation.id).then(data => {
|
||||
this.make_edit_form(translation, data);
|
||||
});
|
||||
}
|
||||
|
||||
get_additional_info(source_id) {
|
||||
frappe.dom.freeze('Fetching...');
|
||||
return frappe.xcall('frappe.translate.get_source_additional_info', {
|
||||
source: source_id,
|
||||
language: this.page.fields_dict['language'].get_value()
|
||||
}).finally(frappe.dom.unfreeze);
|
||||
}
|
||||
|
||||
make_edit_form(translation, { contributions, positions }) {
|
||||
if (!this.form) {
|
||||
this.form = new frappe.ui.FieldGroup({
|
||||
fields: [
|
||||
{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'header',
|
||||
read_only: 1
|
||||
},
|
||||
{
|
||||
fieldtype: 'Data',
|
||||
fieldname: 'id',
|
||||
hidden: 1
|
||||
},
|
||||
{
|
||||
label: 'Source Text',
|
||||
fieldtype: 'Code',
|
||||
fieldname: 'source_text',
|
||||
read_only: 1,
|
||||
enable_copy_button: 1
|
||||
},
|
||||
{
|
||||
label: 'Context',
|
||||
fieldtype: 'Code',
|
||||
fieldname: 'context',
|
||||
read_only: 1
|
||||
},
|
||||
{
|
||||
label: 'DocType',
|
||||
fieldtype: 'Data',
|
||||
fieldname: 'doctype',
|
||||
read_only: 1
|
||||
},
|
||||
{
|
||||
label: 'Translated Text',
|
||||
fieldtype: 'Small Text',
|
||||
fieldname: 'translated_text',
|
||||
},
|
||||
{
|
||||
label: 'Suggest',
|
||||
fieldtype: 'Button',
|
||||
click: () => {
|
||||
let { id, translated_text, source_text } = this.form.get_values();
|
||||
let existing_value = this.form.translation_dict.translated_text;
|
||||
if (
|
||||
is_null(translated_text) ||
|
||||
existing_value === translated_text
|
||||
) {
|
||||
delete this.edited_translations[id];
|
||||
} else if (existing_value !== translated_text) {
|
||||
this.edited_translations[id] = {
|
||||
id,
|
||||
translated_text,
|
||||
source_text
|
||||
};
|
||||
}
|
||||
this.update_header();
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldtype: 'Section Break',
|
||||
fieldname: 'contributed_translations_section',
|
||||
label: 'Contributed Translations'
|
||||
},
|
||||
{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'contributed_translations'
|
||||
},
|
||||
{
|
||||
fieldtype: 'Section Break',
|
||||
collapsible: 1,
|
||||
label: 'Occurences in source code'
|
||||
},
|
||||
{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'positions'
|
||||
},
|
||||
],
|
||||
body: this.wrapper.find('.translation-edit-form')
|
||||
});
|
||||
|
||||
this.form.make();
|
||||
this.setup_header();
|
||||
}
|
||||
|
||||
this.form.set_values(translation);
|
||||
this.form.translation_dict = translation;
|
||||
this.form.set_df_property('doctype', 'hidden', !translation.doctype);
|
||||
this.form.set_df_property('context', 'hidden', !translation.context);
|
||||
this.set_status(translation);
|
||||
|
||||
this.setup_contributions(contributions);
|
||||
this.setup_positions(positions);
|
||||
}
|
||||
|
||||
setup_header() {
|
||||
this.form.get_field('header').$wrapper.html(`<div>
|
||||
<span class="translation-status"></span>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
set_status(translation) {
|
||||
this.form.get_field('header').$wrapper.find('.translation-status').html(`
|
||||
<span class="indicator ${this.get_indicator_color(translation)} text-muted">
|
||||
${this.get_indicator_status_text(translation)}
|
||||
</span>
|
||||
`);
|
||||
}
|
||||
|
||||
setup_positions(positions) {
|
||||
let position_dom = '';
|
||||
if (positions && positions.length) {
|
||||
position_dom = positions.map(position => {
|
||||
if (position.path.startsWith('DocType: ')) {
|
||||
return `<div>
|
||||
<span class="text-muted">${position.path}</span>
|
||||
</div>`;
|
||||
} else {
|
||||
return `<div>
|
||||
<a
|
||||
class="text-muted"
|
||||
target="_blank"
|
||||
href="${this.get_code_url(position.path, position.line_no, position.app)}">
|
||||
${position.path}
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
this.form.get_field('positions').$wrapper.html(position_dom);
|
||||
}
|
||||
|
||||
setup_contributions(contributions) {
|
||||
const contributions_exists = contributions && contributions.length;
|
||||
if (contributions_exists) {
|
||||
let contributions_html = contributions.map(c => {
|
||||
return `
|
||||
<div class="contributed-translation flex justify-between align-center">
|
||||
<div class="ellipsis">${c.translated}</div>
|
||||
<div class="text-muted small">
|
||||
${comment_when(c.creation)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
this.form.get_field('contributed_translations').html(contributions_html);
|
||||
}
|
||||
this.form.set_df_property('contributed_translations_section', 'hidden', !contributions_exists);
|
||||
}
|
||||
show_confirmation_dialog() {
|
||||
this.confirmation_dialog = new frappe.ui.Dialog({
|
||||
fields: [
|
||||
{
|
||||
label: __('Language'),
|
||||
fieldname: 'language',
|
||||
fieldtype: 'Data',
|
||||
read_only: 1,
|
||||
bold: 1,
|
||||
default: this.language
|
||||
},
|
||||
{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'edited_translations'
|
||||
}
|
||||
],
|
||||
title: __('Confirm Translations'),
|
||||
no_submit_on_enter: true,
|
||||
primary_action_label: __('Submit'),
|
||||
primary_action: values => {
|
||||
this.create_translations(values).then(this.confirmation_dialog.hide());
|
||||
}
|
||||
});
|
||||
this.confirmation_dialog.get_field('edited_translations').html(`
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th>${__('Source Text')}</th>
|
||||
<th>${__('Translated Text')}</th>
|
||||
</tr>
|
||||
${Object.values(this.edited_translations).map(t => `
|
||||
<tr>
|
||||
<td>${t.source_text}</td>
|
||||
<td>${t.translated_text}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</table>
|
||||
`);
|
||||
this.confirmation_dialog.show();
|
||||
}
|
||||
create_translations() {
|
||||
frappe.dom.freeze(__('Submitting...'));
|
||||
return frappe
|
||||
.xcall(
|
||||
'frappe.core.doctype.translation.translation.create_translations',
|
||||
{
|
||||
translation_map: this.edited_translations,
|
||||
language: this.language
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
frappe.dom.unfreeze();
|
||||
frappe.show_alert(__('Successfully Submitted!'));
|
||||
this.edited_translations = {};
|
||||
this.update_header();
|
||||
this.fetch_messages_then_render();
|
||||
})
|
||||
.finally(() => frappe.dom.unfreeze());
|
||||
}
|
||||
|
||||
setup_local_contributions() {
|
||||
// TODO: Refactor
|
||||
frappe
|
||||
.xcall('frappe.translate.get_contributions', {
|
||||
language: this.language
|
||||
})
|
||||
.then(messages => {
|
||||
let template = message => `
|
||||
<div
|
||||
class="translation-item"
|
||||
data-message-id="${encodeURIComponent(message.name)}"
|
||||
data-action="show_translation_status_modal">
|
||||
<div class="bold ellipsis">
|
||||
<span class="indicator ${this.get_contribution_indicator_color(message)}">
|
||||
<span>${frappe.utils.escape_html(message.source_text)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let html = messages.map(template).join('');
|
||||
this.wrapper.find('.translation-item-tr').html(html);
|
||||
});
|
||||
}
|
||||
|
||||
show_translation_status_modal(e, $el) {
|
||||
let message_id = decodeURIComponent($el.data('message-id'));
|
||||
|
||||
frappe.xcall('frappe.translate.get_contribution_status', { message_id })
|
||||
.then(doc => {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __('Contribution Status'),
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'source_message',
|
||||
label: __('Source Message'),
|
||||
fieldtype: 'Data',
|
||||
read_only: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'translated',
|
||||
label: __('Translated Message'),
|
||||
fieldtype: 'Data',
|
||||
read_only: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'contribution_status',
|
||||
label: __('Contribution Status'),
|
||||
fieldtype: 'Data',
|
||||
read_only: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'modified_by',
|
||||
label: __('Verified By'),
|
||||
fieldtype: 'Data',
|
||||
read_only: 1,
|
||||
depends_on: doc => {
|
||||
return doc.contribution_status == 'Verified';
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
d.set_values(doc);
|
||||
d.show();
|
||||
});
|
||||
}
|
||||
|
||||
update_header() {
|
||||
let edited_translations_count = Object.keys(this.edited_translations)
|
||||
.length;
|
||||
if (edited_translations_count) {
|
||||
this.page.set_indicator(
|
||||
__('{0} translations pending', [edited_translations_count]),
|
||||
'orange'
|
||||
);
|
||||
} else {
|
||||
this.page.set_indicator('');
|
||||
}
|
||||
this.page.btn_primary.prop('disabled', !edited_translations_count);
|
||||
}
|
||||
|
||||
get_indicator_color(message_obj) {
|
||||
return !message_obj.translated ? 'red' : message_obj.translated_by_google ? 'orange' : 'blue';
|
||||
}
|
||||
|
||||
get_indicator_status_text(message_obj) {
|
||||
if (!message_obj.translated) {
|
||||
return __('Untranslated');
|
||||
} else if (message_obj.translated_by_google) {
|
||||
return __('Google Translation');
|
||||
} else {
|
||||
return __('Community Contribution');
|
||||
}
|
||||
}
|
||||
|
||||
get_contribution_indicator_color(message_obj) {
|
||||
return message_obj.contribution_status == 'Pending' ? 'orange' : 'green';
|
||||
}
|
||||
|
||||
get_code_url(path, line_no, app) {
|
||||
const code_path = path.substring(`apps/${app}`.length);
|
||||
return `https://github.com/frappe/${app}/blob/develop/${code_path}#L${line_no}`;
|
||||
}
|
||||
}
|
||||
26
frappe/desk/page/translation_tool/translation_tool.json
Normal file
26
frappe/desk/page/translation_tool/translation_tool.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"content": null,
|
||||
"creation": "2020-01-30 15:16:12.136323",
|
||||
"docstatus": 0,
|
||||
"doctype": "Page",
|
||||
"idx": 0,
|
||||
"modified": "2020-01-30 15:16:23.273733",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "translation-tool",
|
||||
"owner": "Administrator",
|
||||
"page_name": "Translation Tool",
|
||||
"roles": [
|
||||
{
|
||||
"role": "System Manager"
|
||||
},
|
||||
{
|
||||
"role": "Translator"
|
||||
}
|
||||
],
|
||||
"script": null,
|
||||
"standard": "Yes",
|
||||
"style": null,
|
||||
"system_page": 1,
|
||||
"title": "Translation Tool"
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ class UserProfile {
|
|||
}
|
||||
|
||||
setup_user_search() {
|
||||
this.$user_search_button = this.page.set_secondary_action('Change User', () => {
|
||||
this.$user_search_button = this.page.set_secondary_action(__('Change User'), () => {
|
||||
this.show_user_search_dialog();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
"fieldname": "email_id",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Email Address",
|
||||
"options": "Email",
|
||||
"reqd": 1
|
||||
|
|
@ -90,14 +91,12 @@
|
|||
"default": "0",
|
||||
"fieldname": "awaiting_password",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Awaiting password"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ascii_encode_password",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Use ASCII encoding for password"
|
||||
},
|
||||
{
|
||||
|
|
@ -116,6 +115,8 @@
|
|||
"fieldname": "domain",
|
||||
"fieldtype": "Link",
|
||||
"label": "Domain",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"options": "Email Domain"
|
||||
},
|
||||
{
|
||||
|
|
@ -424,6 +425,10 @@
|
|||
"role": "System Manager",
|
||||
"set_user_permissions": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Inbox User"
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
|
|
|||
|
|
@ -96,14 +96,24 @@ def create_email_flag_queue(names, action):
|
|||
update_modified=False)
|
||||
mark_as_seen_unseen(name, action)
|
||||
|
||||
@frappe.whitelist()
|
||||
def mark_as_closed_open(communication, status):
|
||||
"""Set status to open or close"""
|
||||
frappe.db.set_value("Communication", communication, "status", status)
|
||||
|
||||
@frappe.whitelist()
|
||||
def move_email(communication, email_account):
|
||||
"""Move email to another email account."""
|
||||
frappe.db.set_value("Communication", communication, "email_account", email_account)
|
||||
|
||||
@frappe.whitelist()
|
||||
def mark_as_trash(communication):
|
||||
"""set email status to trash"""
|
||||
"""Set email status to trash."""
|
||||
frappe.db.set_value("Communication", communication, "email_status", "Trash")
|
||||
|
||||
@frappe.whitelist()
|
||||
def mark_as_spam(communication, sender):
|
||||
""" set email status to spam """
|
||||
"""Set email status to spam."""
|
||||
email_rule = frappe.db.get_value("Email Rule", { "email_id": sender })
|
||||
if not email_rule:
|
||||
frappe.get_doc({
|
||||
|
|
@ -118,4 +128,4 @@ def link_communication_to_document(doc, reference_doctype, reference_name, ignor
|
|||
doc.reference_doctype = reference_doctype
|
||||
doc.reference_name = reference_name
|
||||
doc.status = "Linked"
|
||||
doc.save(ignore_permissions=True)
|
||||
doc.save(ignore_permissions=True)
|
||||
|
|
|
|||
|
|
@ -75,22 +75,6 @@ This is the text version of this email
|
|||
else:
|
||||
self.assertTrue(True)
|
||||
|
||||
def test_rfc_5322_header_is_wrapped_at_998_chars(self):
|
||||
# unfortunately the db can only hold 140 chars so this can't be tested properly. test at max chars anyway.
|
||||
email = get_email_queue(
|
||||
recipients=['test@example.com'],
|
||||
sender='me@example.com',
|
||||
subject='Test Subject',
|
||||
content='<h1>Whatever</h1>',
|
||||
text_content='whatever',
|
||||
message_id="a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
|
||||
".really.long.message.id.that.should.not.wrap.unti")
|
||||
result = safe_decode(prepare_message(email=email, recipient='test@test.com',
|
||||
recipients_list=[]))
|
||||
self.assertTrue(
|
||||
"a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
|
||||
".really.long.message.id.that.should.not.wrap.unti" in result)
|
||||
|
||||
def test_image(self):
|
||||
img_signature = '''
|
||||
Content-Type: image/png
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2019-09-27 12:46:50.165135",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"local_fieldname",
|
||||
"mapping_type",
|
||||
"mapping",
|
||||
"remote_value_filters",
|
||||
"column_break_5",
|
||||
"remote_fieldname",
|
||||
"is_child_table",
|
||||
"child_table_mapping"
|
||||
"default_value"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "remote_fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Remote Fieldname",
|
||||
"reqd": 1
|
||||
"label": "Remote Fieldname"
|
||||
},
|
||||
{
|
||||
"fieldname": "local_fieldname",
|
||||
|
|
@ -25,21 +28,39 @@
|
|||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_child_table",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Child Table"
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.is_child_table == 1",
|
||||
"fieldname": "child_table_mapping",
|
||||
"fieldname": "default_value",
|
||||
"fieldtype": "Data",
|
||||
"label": "Default Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "mapping_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Mapping Type",
|
||||
"options": "\nChild Table\nDocument"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.mapping_type;",
|
||||
"fieldname": "mapping",
|
||||
"fieldtype": "Link",
|
||||
"label": "Child Table Mapping",
|
||||
"label": "Mapping",
|
||||
"options": "Document Type Mapping"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.mapping_type==\"Document\";",
|
||||
"fieldname": "remote_value_filters",
|
||||
"fieldtype": "Code",
|
||||
"label": "Remote Value Filters",
|
||||
"mandatory_depends_on": "eval:doc.mapping_type===\"Document\";",
|
||||
"options": "JSON"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-10-09 08:26:06.457122",
|
||||
"links": [],
|
||||
"modified": "2020-03-19 13:56:36.223799",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Event Streaming",
|
||||
"name": "Document Type Field Mapping",
|
||||
|
|
|
|||
|
|
@ -5,35 +5,168 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json
|
||||
from frappe import _
|
||||
from six import iteritems
|
||||
from frappe.model.document import Document
|
||||
|
||||
from frappe.model import default_fields
|
||||
|
||||
class DocumentTypeMapping(Document):
|
||||
def get_mapped_doc(self, update):
|
||||
doc = frappe._dict(json.loads(update))
|
||||
def validate(self):
|
||||
self.validate_inner_mapping()
|
||||
|
||||
def validate_inner_mapping(self):
|
||||
meta = frappe.get_meta(self.local_doctype)
|
||||
for field_map in self.field_mapping:
|
||||
if field_map.local_fieldname not in default_fields:
|
||||
field = meta.get_field(field_map.local_fieldname)
|
||||
if not field:
|
||||
frappe.throw(_('Row #{0}: Invalid Local Fieldname').format(field_map.idx))
|
||||
|
||||
fieldtype = field.get('fieldtype')
|
||||
if fieldtype in ['Link', 'Dynamic Link', 'Table']:
|
||||
if not field_map.mapping and not field_map.default_value:
|
||||
msg = _('Row #{0}: Please set Mapping or Default Value for the field {1} since its a dependency field').format(
|
||||
field_map.idx, frappe.bold(field_map.local_fieldname))
|
||||
frappe.throw(msg, title='Inner Mapping Missing')
|
||||
|
||||
if field_map.mapping_type == 'Document' and not field_map.remote_value_filters:
|
||||
msg = _('Row #{0}: Please set remote value filters for the field {1} to fetch the unique remote dependency document').format(
|
||||
field_map.idx, frappe.bold(field_map.remote_fieldname))
|
||||
frappe.throw(msg, title='Remote Value Filters Missing')
|
||||
|
||||
def get_mapping(self, doc, producer_site, update_type):
|
||||
remote_fields = []
|
||||
# list of tuples (local_fieldname, dependent_doc)
|
||||
dependencies = []
|
||||
|
||||
for mapping in self.field_mapping:
|
||||
if doc.get(mapping.remote_fieldname):
|
||||
if mapping.mapping_type == 'Document':
|
||||
if not mapping.default_value:
|
||||
dependency = self.get_mapped_dependency(mapping, producer_site, doc)
|
||||
if dependency:
|
||||
dependencies.append((mapping.local_fieldname, dependency))
|
||||
else:
|
||||
doc[mapping.local_fieldname] = mapping.default_value
|
||||
|
||||
if mapping.is_child_table:
|
||||
doc[mapping.local_fieldname] = self.get_mapped_child_table_docs(mapping.child_table_mapping, doc[mapping.remote_fieldname])
|
||||
if mapping.mapping_type == 'Child Table' and update_type != 'Update':
|
||||
doc[mapping.local_fieldname] = get_mapped_child_table_docs(mapping.mapping, doc[mapping.remote_fieldname], producer_site)
|
||||
else:
|
||||
# copy value into local fieldname key and remove remote fieldname key
|
||||
doc[mapping.local_fieldname] = doc[mapping.remote_fieldname]
|
||||
doc.pop(mapping.remote_fieldname, None)
|
||||
|
||||
doc['doctype'] = self.local_doctype
|
||||
return frappe.as_json(doc)
|
||||
if mapping.local_fieldname != mapping.remote_fieldname:
|
||||
remote_fields.append(mapping.remote_fieldname)
|
||||
|
||||
if not doc.get(mapping.remote_fieldname) and mapping.default_value and update_type != 'Update':
|
||||
doc[mapping.local_fieldname] = mapping.default_value
|
||||
|
||||
#remove the remote fieldnames
|
||||
for field in remote_fields:
|
||||
doc.pop(field, None)
|
||||
|
||||
if update_type != 'Update':
|
||||
doc['doctype'] = self.local_doctype
|
||||
|
||||
mapping = {'doc': frappe.as_json(doc)}
|
||||
if len(dependencies):
|
||||
mapping['dependencies'] = dependencies
|
||||
return mapping
|
||||
|
||||
|
||||
def get_mapped_child_table_docs(child_map, table_entries):
|
||||
def get_mapped_update(self, update, producer_site):
|
||||
update_diff = frappe._dict(json.loads(update.data))
|
||||
mapping = update_diff
|
||||
dependencies = []
|
||||
if update_diff.changed:
|
||||
doc_map = self.get_mapping(update_diff.changed, producer_site, 'Update')
|
||||
mapped_doc = doc_map.get('doc')
|
||||
mapping.changed = json.loads(mapped_doc)
|
||||
if doc_map.get('dependencies'):
|
||||
dependencies += doc_map.get('dependencies')
|
||||
|
||||
if update_diff.removed:
|
||||
mapping = self.map_rows_removed(update_diff, mapping)
|
||||
if update_diff.added:
|
||||
mapping = self.map_rows(update_diff, mapping, producer_site, operation='added')
|
||||
if update_diff.row_changed:
|
||||
mapping = self.map_rows(update_diff, mapping, producer_site, operation='row_changed')
|
||||
|
||||
update = {'doc': frappe.as_json(mapping)}
|
||||
if len(dependencies):
|
||||
update['dependencies'] = dependencies
|
||||
return update
|
||||
|
||||
def get_mapped_dependency(self, mapping, producer_site, doc):
|
||||
inner_mapping = frappe.get_doc('Document Type Mapping', mapping.mapping)
|
||||
filters = json.loads(mapping.remote_value_filters)
|
||||
for key, value in iteritems(filters):
|
||||
if value.startswith('eval:'):
|
||||
val = frappe.safe_eval(value[5:], dict(frappe=frappe))
|
||||
filters[key] = val
|
||||
if doc.get(value):
|
||||
filters[key] = doc.get(value)
|
||||
matching_docs = producer_site.get_doc(inner_mapping.remote_doctype, filters=filters)
|
||||
if len(matching_docs):
|
||||
remote_docname = matching_docs[0].get('name')
|
||||
remote_doc = producer_site.get_doc(inner_mapping.remote_doctype, remote_docname)
|
||||
doc = inner_mapping.get_mapping(remote_doc, producer_site, 'Insert').get('doc')
|
||||
return doc
|
||||
return
|
||||
|
||||
def map_rows_removed(self, update_diff, mapping):
|
||||
removed = []
|
||||
mapping['removed'] = update_diff.removed
|
||||
for key, value in iteritems(update_diff.removed.copy()):
|
||||
local_table_name = frappe.db.get_value('Document Type Field Mapping', {
|
||||
'remote_fieldname': key,
|
||||
'parent': self.name
|
||||
},'local_fieldname')
|
||||
mapping.removed[local_table_name] = value
|
||||
if local_table_name != key:
|
||||
removed.append(key)
|
||||
|
||||
#remove the remote fieldnames
|
||||
for field in removed:
|
||||
mapping.removed.pop(field, None)
|
||||
return mapping
|
||||
|
||||
def map_rows(self, update_diff, mapping, producer_site, operation):
|
||||
remote_fields = []
|
||||
for tablename, entries in iteritems(update_diff.get(operation).copy()):
|
||||
local_table_name = frappe.db.get_value('Document Type Field Mapping', {'remote_fieldname': tablename}, 'local_fieldname')
|
||||
table_map = frappe.db.get_value('Document Type Field Mapping', {'local_fieldname': local_table_name, 'parent': self.name}, 'mapping')
|
||||
table_map = frappe.get_doc('Document Type Mapping', table_map)
|
||||
docs = []
|
||||
for entry in entries:
|
||||
mapped_doc = table_map.get_mapping(entry, producer_site, 'Update').get('doc')
|
||||
docs.append(json.loads(mapped_doc))
|
||||
mapping.get(operation)[local_table_name] = docs
|
||||
if local_table_name != tablename:
|
||||
remote_fields.append(tablename)
|
||||
|
||||
# remove the remote fieldnames
|
||||
for field in remote_fields:
|
||||
mapping.get(operation).pop(field, None)
|
||||
|
||||
return mapping
|
||||
|
||||
def get_mapped_child_table_docs(child_map, table_entries, producer_site):
|
||||
"""Get mapping for child doctypes"""
|
||||
child_map = frappe.get_doc('Document Type Mapping', child_map)
|
||||
mapped_entries = []
|
||||
remote_fields = []
|
||||
for child_doc in table_entries:
|
||||
for mapping in child_map.field_mapping:
|
||||
if child_doc.get(mapping.remote_fieldname):
|
||||
child_doc[mapping.local_fieldname] = child_doc[mapping.remote_fieldname]
|
||||
child_doc.pop(mapping.remote_fieldname, None)
|
||||
child_doc['doctype'] = child_map.local_doctype
|
||||
if mapping.local_fieldname != mapping.remote_fieldname:
|
||||
child_doc.pop(mapping.remote_fieldname, None)
|
||||
mapped_entries.append(child_doc)
|
||||
|
||||
#remove the remote fieldnames
|
||||
for field in remote_fields:
|
||||
child_doc.pop(field, None)
|
||||
|
||||
child_doc['doctype'] = child_map.local_doctype
|
||||
return mapped_entries
|
||||
|
|
|
|||
|
|
@ -28,11 +28,12 @@ class EventConsumer(Document):
|
|||
def update_consumer_status(self):
|
||||
consumer_site = get_consumer_site(self.callback_url)
|
||||
event_producer = consumer_site.get_doc('Event Producer', get_url())
|
||||
event_producer = frappe._dict(event_producer)
|
||||
config = event_producer.producer_doctypes
|
||||
event_producer.producer_doctypes = []
|
||||
for entry in config:
|
||||
if entry.get('has_mapping'):
|
||||
ref_doctype = consumer_site.get_value('Document Type Mapping', entry.get('mapping'), 'remote_doctype')
|
||||
ref_doctype = consumer_site.get_value('Document Type Mapping', 'remote_doctype', entry.get('mapping')).get('remote_doctype')
|
||||
else:
|
||||
ref_doctype = entry.get('ref_doctype')
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ class EventProducer(Document):
|
|||
if self.is_producer_online():
|
||||
producer_site = get_producer_site(self.producer_url)
|
||||
event_consumer = producer_site.get_doc('Event Consumer', get_url())
|
||||
event_consumer = frappe._dict(event_consumer)
|
||||
if event_consumer:
|
||||
config = event_consumer.consumer_doctypes
|
||||
event_consumer.consumer_doctypes = []
|
||||
|
|
@ -172,7 +173,7 @@ def pull_from_node(event_producer):
|
|||
mapping = mapping_config.get(update.ref_doctype)
|
||||
if mapping:
|
||||
update.mapping = mapping
|
||||
update = get_mapped_update(update)
|
||||
update = get_mapped_update(update, producer_site)
|
||||
if not update.update_type == 'Delete':
|
||||
update.data = json.loads(update.data)
|
||||
|
||||
|
|
@ -215,6 +216,7 @@ def sync(update, producer_site, event_producer, in_retry=False):
|
|||
log_event_sync(update, event_producer.name, 'Failed', frappe.get_traceback())
|
||||
|
||||
frappe.db.set_value('Event Producer', event_producer.name, 'last_update', update.creation)
|
||||
event_producer.reload()
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
|
|
@ -224,7 +226,15 @@ def set_insert(update, producer_site, event_producer):
|
|||
# doc already created
|
||||
return
|
||||
doc = frappe.get_doc(update.data)
|
||||
sync_dependencies(doc, producer_site)
|
||||
|
||||
if update.mapping:
|
||||
if update.get('dependencies'):
|
||||
dependencies_created = sync_mapped_dependencies(update.dependencies, producer_site)
|
||||
for fieldname, value in iteritems(dependencies_created):
|
||||
doc.update({ fieldname : value })
|
||||
else:
|
||||
sync_dependencies(doc, producer_site)
|
||||
|
||||
if update.use_same_name:
|
||||
doc.insert(set_name=update.docname, set_child_names=False)
|
||||
else:
|
||||
|
|
@ -237,24 +247,28 @@ def set_insert(update, producer_site, event_producer):
|
|||
def set_update(update, producer_site):
|
||||
"""Sync update type update"""
|
||||
local_doc = get_local_doc(update)
|
||||
try:
|
||||
if local_doc:
|
||||
data = frappe._dict(update.data)
|
||||
if local_doc:
|
||||
data = frappe._dict(update.data)
|
||||
|
||||
if data.changed:
|
||||
local_doc.update(data.changed)
|
||||
if data.removed:
|
||||
update_row_removed(local_doc, data.removed)
|
||||
if data.row_changed:
|
||||
update_row_changed(local_doc, data.row_changed)
|
||||
if data.added:
|
||||
local_doc = update_row_added(local_doc, data.added)
|
||||
if data.changed:
|
||||
local_doc.update(data.changed)
|
||||
if data.removed:
|
||||
update_row_removed(local_doc, data.removed)
|
||||
if data.row_changed:
|
||||
update_row_changed(local_doc, data.row_changed)
|
||||
if data.added:
|
||||
local_doc = update_row_added(local_doc, data.added)
|
||||
|
||||
local_doc.save()
|
||||
local_doc.db_update_all()
|
||||
if update.mapping:
|
||||
if update.get('dependencies'):
|
||||
dependencies_created = sync_mapped_dependencies(update.dependencies, producer_site)
|
||||
for fieldname, value in iteritems(dependencies_created):
|
||||
local_doc.update({ fieldname : value })
|
||||
else:
|
||||
sync_dependencies(local_doc, producer_site)
|
||||
|
||||
except frappe.DoesNotExistError:
|
||||
sync_dependencies(local_doc, producer_site)
|
||||
local_doc.save()
|
||||
local_doc.db_update_all()
|
||||
|
||||
|
||||
def update_row_removed(local_doc, removed):
|
||||
|
|
@ -343,6 +357,7 @@ def sync_dependencies(document, producer_site):
|
|||
child_table = doc.get(df.fieldname)
|
||||
for entry in child_table:
|
||||
child_doc = producer_site.get_doc(entry.doctype, entry.name)
|
||||
child_doc = frappe._dict(child_doc)
|
||||
set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site)
|
||||
|
||||
def sync_link_dependencies(doc, link_fields, producer_site):
|
||||
|
|
@ -394,6 +409,19 @@ def sync_dependencies(document, producer_site):
|
|||
dependencies[document] = False
|
||||
|
||||
|
||||
def sync_mapped_dependencies(dependencies, producer_site):
|
||||
dependencies_created = {}
|
||||
for entry in dependencies:
|
||||
doc = frappe._dict(json.loads(entry[1]))
|
||||
docname = frappe.db.exists(doc.doctype, doc.name)
|
||||
if not docname:
|
||||
doc = frappe.get_doc(doc).insert(set_child_names=False)
|
||||
dependencies_created[entry[0]] = doc.name
|
||||
else:
|
||||
dependencies_created[entry[0]] = docname
|
||||
|
||||
return dependencies_created
|
||||
|
||||
def log_event_sync(update, event_producer, sync_status, error=None):
|
||||
"""Log event update received with the sync_status as Synced or Failed"""
|
||||
doc = frappe.new_doc('Event Sync Log')
|
||||
|
|
@ -414,12 +442,20 @@ def log_event_sync(update, event_producer, sync_status, error=None):
|
|||
doc.insert()
|
||||
|
||||
|
||||
def get_mapped_update(update):
|
||||
def get_mapped_update(update, producer_site):
|
||||
"""get the new update document with mapped fields"""
|
||||
mapping = frappe.get_doc('Document Type Mapping', update.mapping)
|
||||
if update.update_type != 'Delete':
|
||||
update.data = mapping.get_mapped_doc(update.data)
|
||||
update.ref_doctype = mapping.local_doctype
|
||||
if update.update_type == 'Create':
|
||||
doc = frappe._dict(json.loads(update.data))
|
||||
mapped_update = mapping.get_mapping(doc, producer_site, update.update_type)
|
||||
update.data = mapped_update.get('doc')
|
||||
update.dependencies = mapped_update.get('dependencies', None)
|
||||
elif update.update_type == 'Update':
|
||||
mapped_update = mapping.get_mapped_update(update, producer_site)
|
||||
update.data = mapped_update.get('doc')
|
||||
update.dependencies = mapped_update.get('dependencies', None)
|
||||
|
||||
update['ref_doctype'] = mapping.local_doctype
|
||||
return update
|
||||
|
||||
|
||||
|
|
@ -436,11 +472,11 @@ def new_event_notification(producer_url):
|
|||
def resync(update):
|
||||
"""Retry syncing update if failed"""
|
||||
update = frappe._dict(json.loads(update))
|
||||
if update.mapping:
|
||||
update = get_mapped_update(update)
|
||||
update.data = json.loads(update.data)
|
||||
producer_site = get_producer_site(update.event_producer)
|
||||
event_producer = frappe.get_doc('Event Producer', update.event_producer)
|
||||
if update.mapping:
|
||||
update = get_mapped_update(update, producer_site)
|
||||
update.data = json.loads(update.data)
|
||||
return sync(update, producer_site, event_producer, in_retry=True)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,17 @@ from __future__ import unicode_literals
|
|||
|
||||
import frappe
|
||||
import unittest
|
||||
import time
|
||||
import json
|
||||
from frappe.frappeclient import FrappeClient
|
||||
from frappe.event_streaming.doctype.event_producer.event_producer import pull_from_node
|
||||
|
||||
def create_event_producer(producer_url):
|
||||
event_producer = frappe.new_doc('Event Producer')
|
||||
producer = frappe.db.exists('Event Producer', producer_url)
|
||||
if producer:
|
||||
event_producer = frappe.get_doc('Event Producer', producer)
|
||||
else:
|
||||
event_producer = frappe.new_doc('Event Producer')
|
||||
event_producer.producer_doctypes = []
|
||||
event_producer.producer_url = producer_url
|
||||
event_producer.append('producer_doctypes', {
|
||||
'ref_doctype': 'ToDo',
|
||||
|
|
@ -21,13 +26,14 @@ def create_event_producer(producer_url):
|
|||
'use_same_name': 1
|
||||
})
|
||||
event_producer.user = 'Administrator'
|
||||
event_producer.insert()
|
||||
event_producer.save()
|
||||
event_producer.reload()
|
||||
|
||||
|
||||
class TestEventProducer(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.producer_url = 'http://test_site_producer:8000'
|
||||
if not frappe.db.exists('Event Producer', self.producer_url):
|
||||
create_event_producer(self.producer_url)
|
||||
create_event_producer(self.producer_url)
|
||||
frappe.db.sql('delete from tabToDo')
|
||||
frappe.db.sql('delete from tabNote')
|
||||
|
||||
|
|
@ -123,6 +129,7 @@ class TestEventProducer(unittest.TestCase):
|
|||
'use_same_name': 1
|
||||
})
|
||||
event_producer.save()
|
||||
event_producer.reload()
|
||||
|
||||
producer = self.get_remote_site()
|
||||
producer_link_doc = frappe.get_doc(dict(doctype='Note', title='Test Dynamic Link 1'))
|
||||
|
|
@ -140,14 +147,6 @@ class TestEventProducer(unittest.TestCase):
|
|||
self.assertTrue(frappe.db.exists('Note', producer_link_doc.name))
|
||||
self.assertEqual(producer_link_doc.name, frappe.db.get_value('ToDo', producer_doc.name, 'reference_name'))
|
||||
|
||||
#subscribe again
|
||||
event_producer = frappe.get_doc('Event Producer', self.producer_url)
|
||||
event_producer.append('producer_doctypes', {
|
||||
'ref_doctype': 'Note',
|
||||
'use_same_name': 1
|
||||
})
|
||||
event_producer.save()
|
||||
|
||||
def test_naming_configuration(self):
|
||||
#test with use_same_name = 0
|
||||
event_producer = frappe.get_doc('Event Producer', self.producer_url)
|
||||
|
|
@ -157,21 +156,13 @@ class TestEventProducer(unittest.TestCase):
|
|||
'use_same_name': 0
|
||||
})
|
||||
event_producer.save()
|
||||
event_producer.reload()
|
||||
|
||||
producer = self.get_remote_site()
|
||||
producer_doc = insert_into_producer(producer, 'test different name sync')
|
||||
self.pull_producer_data()
|
||||
self.assertTrue(frappe.db.exists('ToDo', {'remote_docname': producer_doc.name, 'remote_site_name': self.producer_url}))
|
||||
|
||||
event_producer = frappe.get_doc('Event Producer', self.producer_url)
|
||||
event_producer.producer_doctypes = []
|
||||
#set use_same_name back to 1
|
||||
event_producer.append('producer_doctypes', {
|
||||
'ref_doctype': 'ToDo',
|
||||
'use_same_name': 1
|
||||
})
|
||||
event_producer.save()
|
||||
|
||||
def test_update_log(self):
|
||||
producer = self.get_remote_site()
|
||||
producer_doc = insert_into_producer(producer, 'test update log')
|
||||
|
|
@ -186,7 +177,6 @@ class TestEventProducer(unittest.TestCase):
|
|||
|
||||
def pull_producer_data(self):
|
||||
pull_from_node(self.producer_url)
|
||||
time.sleep(1)
|
||||
|
||||
def get_remote_site(self):
|
||||
producer_doc = frappe.get_doc('Event Producer', self.producer_url)
|
||||
|
|
@ -198,6 +188,87 @@ class TestEventProducer(unittest.TestCase):
|
|||
)
|
||||
return producer_site
|
||||
|
||||
def test_mapping(self):
|
||||
event_producer = frappe.get_doc('Event Producer', self.producer_url)
|
||||
event_producer.producer_doctypes = []
|
||||
mapping = [{
|
||||
'local_fieldname': 'description',
|
||||
'remote_fieldname': 'content'
|
||||
}]
|
||||
event_producer.append('producer_doctypes', {
|
||||
'ref_doctype': 'ToDo',
|
||||
'use_same_name': 1,
|
||||
'has_mapping': 1,
|
||||
'mapping': get_mapping('ToDo to Note', 'ToDo', 'Note', mapping)
|
||||
})
|
||||
event_producer.save()
|
||||
event_producer.reload()
|
||||
|
||||
producer = self.get_remote_site()
|
||||
producer_note = frappe.get_doc(dict(doctype='Note', title='Test Mapping', content='Test Mapping'))
|
||||
delete_on_remote_if_exists(producer, 'Note', {'title': producer_note.title})
|
||||
producer_note = producer.insert(producer_note)
|
||||
self.pull_producer_data()
|
||||
#check inserted
|
||||
self.assertTrue(frappe.db.exists('ToDo', {'description': producer_note.content}))
|
||||
|
||||
#update in producer
|
||||
producer_note['content'] = 'test mapped doc update sync'
|
||||
producer_note = producer.update(producer_note)
|
||||
self.pull_producer_data()
|
||||
|
||||
# check updated
|
||||
self.assertTrue(frappe.db.exists('ToDo', {'description': producer_note['content']}))
|
||||
|
||||
producer.delete('Note', producer_note.name)
|
||||
self.pull_producer_data()
|
||||
#check delete
|
||||
self.assertFalse(frappe.db.exists('ToDo', {'description': producer_note.content}))
|
||||
|
||||
def test_inner_mapping(self):
|
||||
event_producer = frappe.get_doc('Event Producer', self.producer_url)
|
||||
event_producer.producer_doctypes = []
|
||||
inner_mapping = [
|
||||
{
|
||||
'local_fieldname':'role_name',
|
||||
'remote_fieldname':'title'
|
||||
}
|
||||
]
|
||||
inner_map = get_mapping('Role to Note Dependency Creation', 'Role', 'Note', inner_mapping)
|
||||
mapping = [
|
||||
{
|
||||
'local_fieldname':'description',
|
||||
'remote_fieldname':'content',
|
||||
},
|
||||
{
|
||||
'local_fieldname': 'role',
|
||||
'remote_fieldname': 'title',
|
||||
'mapping_type': 'Document',
|
||||
'mapping': inner_map,
|
||||
'remote_value_filters': json.dumps({'title': 'title'})
|
||||
}
|
||||
]
|
||||
event_producer.append('producer_doctypes', {
|
||||
'ref_doctype': 'ToDo',
|
||||
'use_same_name': 1,
|
||||
'has_mapping': 1,
|
||||
'mapping': get_mapping('ToDo to Note Mapping', 'ToDo', 'Note', mapping)
|
||||
})
|
||||
event_producer.save()
|
||||
event_producer.reload()
|
||||
|
||||
producer = self.get_remote_site()
|
||||
producer_note = frappe.get_doc(dict(doctype='Note', title='Inner Mapping Tester', content='Test Inner Mapping'))
|
||||
delete_on_remote_if_exists(producer, 'Note', {'title': producer_note.title})
|
||||
producer_note = producer.insert(producer_note)
|
||||
self.pull_producer_data()
|
||||
|
||||
#check dependency inserted
|
||||
self.assertTrue(frappe.db.exists('Role', {'role_name': producer_note.title}))
|
||||
#check doc inserted
|
||||
self.assertTrue(frappe.db.exists('ToDo', {'description': producer_note.content}))
|
||||
|
||||
|
||||
def insert_into_producer(producer, description):
|
||||
#create and insert todo on remote site
|
||||
todo = frappe.get_doc(dict(doctype='ToDo', description=description, assigned_by='Administrator'))
|
||||
|
|
@ -206,4 +277,19 @@ def insert_into_producer(producer, description):
|
|||
def delete_on_remote_if_exists(producer, doctype, filters):
|
||||
remote_doc = producer.get_value(doctype, 'name', filters)
|
||||
if remote_doc:
|
||||
producer.delete(doctype, remote_doc.get('name'))
|
||||
producer.delete(doctype, remote_doc.get('name'))
|
||||
|
||||
def get_mapping(mapping_name, local, remote, field_map):
|
||||
name = frappe.db.exists('Document Type Mapping', mapping_name)
|
||||
if name:
|
||||
doc = frappe.get_doc('Document Type Mapping', name)
|
||||
else:
|
||||
doc = frappe.new_doc('Document Type Mapping')
|
||||
|
||||
doc.mapping_name = mapping_name
|
||||
doc.local_doctype = local
|
||||
doc.remote_doctype = remote
|
||||
for entry in field_map:
|
||||
doc.append('field_mapping', entry)
|
||||
doc.save()
|
||||
return doc.name
|
||||
|
|
@ -200,7 +200,7 @@ class FrappeClient(object):
|
|||
res = self.session.get(self.url + "/api/resource/" + doctype + "/" + name,
|
||||
params=params, verify=self.verify, headers=self.headers)
|
||||
|
||||
return frappe._dict(self.post_process(res))
|
||||
return self.post_process(res)
|
||||
|
||||
def rename_doc(self, doctype, old_name, new_name):
|
||||
'''Rename remote document
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ app_email = "info@frappe.io"
|
|||
|
||||
docs_app = "frappe_io"
|
||||
|
||||
translation_contribution_url = "https://translate.erpnext.com/api/method/translator.api.add_translation"
|
||||
translation_contribution_status = "https://translate.erpnext.com/api/method/translator.api.translation_status"
|
||||
translator_url = "https://translatev2.erpnext.com"
|
||||
|
||||
before_install = "frappe.utils.install.before_install"
|
||||
after_install = "frappe.utils.install.after_install"
|
||||
|
|
|
|||
|
|
@ -1,18 +1,28 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2017-09-04 20:57:20.129205",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enabled",
|
||||
"notify_email",
|
||||
"send_email_for_successful_backup",
|
||||
"frequency",
|
||||
"api_access_section",
|
||||
"access_key_id",
|
||||
"column_break_4",
|
||||
"secret_access_key",
|
||||
"region",
|
||||
"endpoint_url",
|
||||
"notification_section",
|
||||
"notify_email",
|
||||
"column_break_8",
|
||||
"send_email_for_successful_backup",
|
||||
"s3_bucket_details_section",
|
||||
"bucket",
|
||||
"endpoint_url",
|
||||
"column_break_13",
|
||||
"region",
|
||||
"backup_details_section",
|
||||
"frequency",
|
||||
"backup_files",
|
||||
"column_break_18",
|
||||
"backup_limit"
|
||||
],
|
||||
"fields": [
|
||||
|
|
@ -27,6 +37,7 @@
|
|||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Send Notifications To",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -41,6 +52,7 @@
|
|||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Backup Frequency",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"options": "Daily\nWeekly\nMonthly\nNone",
|
||||
"reqd": 1
|
||||
},
|
||||
|
|
@ -49,13 +61,15 @@
|
|||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Access Key ID",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "secret_access_key",
|
||||
"fieldtype": "Password",
|
||||
"in_list_view": 1,
|
||||
"label": "Secret Access Key",
|
||||
"label": "Access Key Secret",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -74,19 +88,70 @@
|
|||
{
|
||||
"fieldname": "bucket",
|
||||
"fieldtype": "Data",
|
||||
"label": "Bucket",
|
||||
"label": "Bucket Name",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"description": "Set to 0 for no limit on the number of backups taken",
|
||||
"fieldname": "backup_limit",
|
||||
"fieldtype": "Int",
|
||||
"label": "Backup Limit",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "enabled",
|
||||
"fieldname": "api_access_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "API Access"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "enabled",
|
||||
"fieldname": "notification_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Notification"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "enabled",
|
||||
"fieldname": "s3_bucket_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "S3 Bucket Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "enabled",
|
||||
"fieldname": "backup_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Backup Details"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Backup public and private files along with the database.",
|
||||
"fieldname": "backup_files",
|
||||
"fieldtype": "Check",
|
||||
"label": "Backup Files"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"modified": "2019-08-22 16:26:04.774571",
|
||||
"links": [],
|
||||
"modified": "2020-04-13 20:57:24.432183",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "S3 Backup Settings",
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ def backup_to_s3():
|
|||
|
||||
doc = frappe.get_single("S3 Backup Settings")
|
||||
bucket = doc.bucket
|
||||
backup_files = cint(doc.backup_files)
|
||||
|
||||
conn = boto3.client(
|
||||
's3',
|
||||
|
|
@ -114,17 +115,22 @@ def backup_to_s3():
|
|||
backup = new_backup(ignore_files=False, backup_path_db=None,
|
||||
backup_path_files=None, backup_path_private_files=None, force=True)
|
||||
db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db))
|
||||
files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files))
|
||||
private_files = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_private_files))
|
||||
if backup_files:
|
||||
files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files))
|
||||
private_files = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_private_files))
|
||||
else:
|
||||
db_filename, files_filename, private_files = get_latest_backup_file(with_files=True)
|
||||
if backup_files:
|
||||
db_filename, files_filename, private_files = get_latest_backup_file(with_files=backup_files)
|
||||
else:
|
||||
db_filename = get_latest_backup_file()
|
||||
|
||||
folder = os.path.basename(db_filename)[:15] + '/'
|
||||
# for adding datetime to folder name
|
||||
|
||||
upload_file_to_s3(db_filename, folder, conn, bucket)
|
||||
upload_file_to_s3(private_files, folder, conn, bucket)
|
||||
upload_file_to_s3(files_filename, folder, conn, bucket)
|
||||
if backup_files:
|
||||
upload_file_to_s3(private_files, folder, conn, bucket)
|
||||
upload_file_to_s3(files_filename, folder, conn, bucket)
|
||||
delete_old_backups(doc.backup_limit, bucket)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ class Webhook(Document):
|
|||
if self.request_structure == "Form URL-Encoded":
|
||||
self.webhook_json = None
|
||||
elif self.request_structure == "JSON":
|
||||
validate_json(self.webhook_json)
|
||||
validate_template(self.webhook_json)
|
||||
self.webhook_data = []
|
||||
|
||||
|
|
@ -130,3 +131,10 @@ def get_webhook_data(doc, webhook):
|
|||
data = json.loads(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def validate_json(string):
|
||||
try:
|
||||
json.loads(string)
|
||||
except (TypeError, ValueError):
|
||||
frappe.throw(_("Request Body consists of an invalid JSON structure"), title=_("Invalid JSON"))
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import frappe.translate
|
|||
import frappe.modules.patch_handler
|
||||
import frappe.model.sync
|
||||
from frappe.utils.fixtures import sync_fixtures
|
||||
from frappe.utils.dashboard import sync_dashboards
|
||||
from frappe.cache_manager import clear_global_cache
|
||||
from frappe.desk.notifications import clear_notifications
|
||||
from frappe.website import render
|
||||
|
|
@ -23,6 +24,7 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False):
|
|||
- run before migrate hooks
|
||||
- run patches
|
||||
- sync doctypes (schema)
|
||||
- sync dashboards
|
||||
- sync fixtures
|
||||
- sync desktop icons
|
||||
- sync web pages (from /www)
|
||||
|
|
@ -53,6 +55,7 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False):
|
|||
frappe.translate.clear_cache()
|
||||
sync_jobs()
|
||||
sync_fixtures()
|
||||
sync_dashboards()
|
||||
sync_customizations()
|
||||
sync_languages()
|
||||
|
||||
|
|
|
|||
|
|
@ -1329,13 +1329,14 @@ def make_event_update_log(doc, update_type):
|
|||
|
||||
def check_doctype_has_consumers(doctype):
|
||||
"""Check if doctype has event consumers for event streaming"""
|
||||
if not frappe.db.exists("DocType", "Event Consumer"):
|
||||
if not frappe.db.exists('DocType', 'Event Consumer'):
|
||||
return False
|
||||
|
||||
event_consumers = frappe.get_all('Event Consumer')
|
||||
for event_consumer in event_consumers:
|
||||
consumer = frappe.get_doc('Event Consumer', event_consumer.name)
|
||||
for entry in consumer.consumer_doctypes:
|
||||
if doctype == entry.ref_doctype and entry.status == 'Approved':
|
||||
return True
|
||||
return False
|
||||
event_consumers = frappe.get_all('Event Consumer Document Type', {
|
||||
'ref_doctype': doctype,
|
||||
'status': 'Approved'
|
||||
}, limit=1)
|
||||
|
||||
if len(event_consumers) and event_consumers[0]:
|
||||
return True
|
||||
return False
|
||||
|
|
@ -45,9 +45,13 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
|
|||
("data_migration", "data_migration_mapping"),
|
||||
("data_migration", "data_migration_plan_mapping"),
|
||||
("data_migration", "data_migration_plan"),
|
||||
("desk", "onboarding_slide_field"),
|
||||
("desk", "onboarding_slide_help_link"),
|
||||
("desk", "onboarding_slide"),
|
||||
("desk", "number_card"),
|
||||
("desk", "dashboard_chart"),
|
||||
("desk", "dashboard"),
|
||||
("desk", "onboarding_permission"),
|
||||
("desk", "onboarding_step"),
|
||||
("desk", "onboarding_step_map"),
|
||||
("desk", "onboarding"),
|
||||
("desk", "desk_card"),
|
||||
("desk", "desk_chart"),
|
||||
("desk", "desk_shortcut"),
|
||||
|
|
@ -80,7 +84,9 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F
|
|||
# load in sequence - warning for devs
|
||||
document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
|
||||
'website_theme', 'web_form', 'web_template', 'notification', 'print_style',
|
||||
'data_migration_mapping', 'data_migration_plan', 'onboarding_slide', 'desk_page']
|
||||
'data_migration_mapping', 'data_migration_plan', 'desk_page',
|
||||
'onboarding_step', 'onboarding']
|
||||
|
||||
for doctype in document_types:
|
||||
doctype_path = os.path.join(start_path, doctype)
|
||||
if os.path.exists(doctype_path):
|
||||
|
|
|
|||
|
|
@ -12,9 +12,13 @@ ignore_values = {
|
|||
"Report": ["disabled", "prepared_report"],
|
||||
"Print Format": ["disabled"],
|
||||
"Notification": ["enabled"],
|
||||
"Print Style": ["disabled"]
|
||||
"Print Style": ["disabled"],
|
||||
"Onboarding": ['is_complete'],
|
||||
"Onboarding Step": ['is_complete', 'is_skipped']
|
||||
}
|
||||
|
||||
ignore_doctypes = [""]
|
||||
|
||||
def import_files(module, dt=None, dn=None, force=False, pre_process=None, reset_permissions=False):
|
||||
if type(module) is list:
|
||||
out = []
|
||||
|
|
@ -92,8 +96,6 @@ def read_doc_from_file(path):
|
|||
|
||||
return doc
|
||||
|
||||
ignore_doctypes = [""]
|
||||
|
||||
def import_doc(docdict, force=False, data_import=False, pre_process=None,
|
||||
ignore_version=None, reset_permissions=False):
|
||||
frappe.flags.in_import = True
|
||||
|
|
@ -114,7 +116,7 @@ def import_doc(docdict, force=False, data_import=False, pre_process=None,
|
|||
ignore = []
|
||||
|
||||
if frappe.db.exists(doc.doctype, doc.name):
|
||||
# import pdb; pdb.set_trace()
|
||||
|
||||
old_doc = frappe.get_doc(doc.doctype, doc.name)
|
||||
|
||||
if doc.doctype in ignore_values:
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class Monitor:
|
|||
|
||||
if self.data.transaction_type == "request":
|
||||
self.data.request.status_code = response.status_code
|
||||
self.data.request.response_length = int(response.headers["Content-Length"])
|
||||
self.data.request.response_length = int(response.headers.get("Content-Length", 0))
|
||||
|
||||
self.store()
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -275,3 +275,6 @@ execute:from frappe.desk.page.setup_wizard.install_fixtures import update_gender
|
|||
frappe.patches.v13_0.website_theme_custom_scss
|
||||
frappe.patches.v13_0.set_existing_dashboard_charts_as_public
|
||||
frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
|
||||
frappe.patches.v13_0.migrate_translation_column_data
|
||||
frappe.patches.v13_0.set_read_times
|
||||
frappe.patches.v13_0.remove_web_view
|
||||
|
|
|
|||
5
frappe/patches/v13_0/migrate_translation_column_data.py
Normal file
5
frappe/patches/v13_0/migrate_translation_column_data.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doctype('Translation')
|
||||
frappe.db.sql("UPDATE `tabTranslation` SET `translated_text`=`target_name`, `source_text`=`source_name`, `contributed`=0")
|
||||
6
frappe/patches/v13_0/remove_web_view.py
Normal file
6
frappe/patches/v13_0/remove_web_view.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.delete_doc_if_exists("DocType", "Web View")
|
||||
frappe.delete_doc_if_exists("DocType", "Web View Component")
|
||||
frappe.delete_doc_if_exists("DocType", "CSS Class")
|
||||
|
|
@ -11,19 +11,11 @@ def execute():
|
|||
fields=["parent"],
|
||||
filters={"role": ['in', ['System Manager', 'Dashboard Manager']], "parenttype": "User"},
|
||||
distinct=True,
|
||||
as_list=True
|
||||
)
|
||||
|
||||
users = tuple(
|
||||
[item if type(item) == str else item.encode('utf8') for sublist in users_with_permission for item in sublist]
|
||||
)
|
||||
users = [item.parent for item in users_with_permission]
|
||||
charts = frappe.db.get_all('Dashboard Chart', filters={'owner': ['in', users]})
|
||||
|
||||
for chart in charts:
|
||||
frappe.db.set_value('Dashboard Chart', chart.name, 'is_public', 1)
|
||||
|
||||
frappe.db.sql("""
|
||||
UPDATE
|
||||
`tabDashboard Chart`
|
||||
SET
|
||||
`tabDashboard Chart`.`is_public`=1
|
||||
WHERE
|
||||
`tabDashboard Chart`.owner in {users}
|
||||
""".format(users=users)
|
||||
)
|
||||
|
|
|
|||
18
frappe/patches/v13_0/set_read_times.py
Normal file
18
frappe/patches/v13_0/set_read_times.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import frappe
|
||||
from frappe.utils import strip_html_tags, markdown
|
||||
from math import ceil
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("website", "doctype", "blog_post")
|
||||
|
||||
for blog in frappe.get_all("Blog Post"):
|
||||
blog = frappe.get_doc("Blog Post", blog.name)
|
||||
frappe.db.set_value("Blog Post", blog.name, "read_time", get_read_time(blog), update_modified=False)
|
||||
|
||||
def get_read_time(blog):
|
||||
content = blog.content or blog.content_html
|
||||
if blog.content_type == "Markdown":
|
||||
content = markdown(blog.content_md)
|
||||
|
||||
total_words = len(strip_html_tags(content or "").split())
|
||||
return ceil(total_words/250)
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.utils import is_image
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
class LetterHead(Document):
|
||||
|
|
@ -45,16 +44,3 @@ class LetterHead(Document):
|
|||
else:
|
||||
frappe.defaults.clear_default('letter_head', self.name)
|
||||
frappe.defaults.clear_default("default_letter_head_content", self.content)
|
||||
|
||||
def create_onboarding_docs(self, args):
|
||||
letterhead = args.get('letterhead')
|
||||
if letterhead:
|
||||
try:
|
||||
frappe.get_doc({
|
||||
'doctype': self.doctype,
|
||||
'image': letterhead,
|
||||
'letter_head_name': _('Standard'),
|
||||
'is_default': 1
|
||||
}).insert()
|
||||
except frappe.NameError:
|
||||
pass
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
{
|
||||
"add_more_button": 0,
|
||||
"app": "ERPNext",
|
||||
"creation": "2019-11-22 13:25:42.892593",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Slide",
|
||||
"domains": [],
|
||||
"help_links": [
|
||||
{
|
||||
"label": "Need Help?",
|
||||
"video_id": "cKZHcx1znMc"
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"image_src": "",
|
||||
"is_completed": 1,
|
||||
"max_count": 0,
|
||||
"modified": "2019-12-09 15:12:45.588567",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Company Letter Head",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Letter Head",
|
||||
"slide_desc": "<p>The letter head will appear across all print formats and PDFs</p>\n<p class=\"text-muted\">Keep it web friendly as 1024px by 128px</p>",
|
||||
"slide_fields": [
|
||||
{
|
||||
"align": "center",
|
||||
"fieldname": "letterhead",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Attach Letterhead",
|
||||
"options": "image",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"slide_order": 20,
|
||||
"slide_title": "Company Letter Head",
|
||||
"slide_type": "Create"
|
||||
}
|
||||
BIN
frappe/public/images/ui-states/success-color.png
Normal file
BIN
frappe/public/images/ui-states/success-color.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
|
|
@ -2,7 +2,7 @@
|
|||
// MIT License. See license.txt
|
||||
|
||||
// library to mange assets (js, css, models, html) etc in the app.
|
||||
// will try and get from localStorge if latest are available
|
||||
// will try and get from localStorage if latest are available
|
||||
// depends on frappe.versions to manage versioning
|
||||
|
||||
frappe.require = function(items, callback) {
|
||||
|
|
|
|||
|
|
@ -96,10 +96,6 @@ frappe.Application = Class.extend({
|
|||
|
||||
this.show_notes();
|
||||
|
||||
if (frappe.boot.is_first_startup) {
|
||||
this.setup_onboarding_wizard();
|
||||
}
|
||||
|
||||
if (frappe.ui.startup_setup_dialog && !frappe.boot.setup_complete) {
|
||||
frappe.ui.startup_setup_dialog.pre_show();
|
||||
frappe.ui.startup_setup_dialog.show();
|
||||
|
|
@ -512,20 +508,6 @@ frappe.Application = Class.extend({
|
|||
});
|
||||
},
|
||||
|
||||
setup_onboarding_wizard: () => {
|
||||
frappe.call('frappe.desk.doctype.onboarding_slide.onboarding_slide.get_onboarding_slides').then(res => {
|
||||
if (res.message) {
|
||||
let slides = res.message;
|
||||
if (slides.length) {
|
||||
this.progress_dialog = new frappe.setup.OnboardingDialog({
|
||||
slides: slides
|
||||
});
|
||||
this.progress_dialog.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setup_analytics: function() {
|
||||
if(window.mixpanel) {
|
||||
window.mixpanel.identify(frappe.session.user);
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ frappe.ui.form.Control = Class.extend({
|
|||
if (!this.doc.__islocal) {
|
||||
new frappe.views.TranslationManager({
|
||||
'df': this.df,
|
||||
'source_name': value,
|
||||
'source_text': value,
|
||||
'target_language': this.doc.language,
|
||||
'doc': this.doc
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({
|
|||
$(this.input_area).find('i').hover((ev) => {
|
||||
const el = $(ev.currentTarget);
|
||||
let star_value = el.data('rating');
|
||||
el.parent().children('i.fa').each( function(e){
|
||||
el.parent().children('i.fa').each( function(e) {
|
||||
if (e < star_value) {
|
||||
$(this).addClass('star-hover');
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,22 @@ Quill.register(FontStyle, true);
|
|||
Quill.register(AlignStyle, true);
|
||||
Quill.register(DirectionStyle, true);
|
||||
|
||||
// replace font tag with span
|
||||
const Inline = Quill.import('blots/inline');
|
||||
|
||||
class CustomColor extends Inline {
|
||||
constructor(domNode, value) {
|
||||
super(domNode, value);
|
||||
this.domNode.style.color = this.domNode.color;
|
||||
domNode.outerHTML = this.domNode.outerHTML.replace(/<font/g, '<span').replace(/<\/font>/g, '</span>');
|
||||
}
|
||||
}
|
||||
|
||||
CustomColor.blotName = "customColor";
|
||||
CustomColor.tagName = "font";
|
||||
|
||||
Quill.register(CustomColor, true);
|
||||
|
||||
frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
|
||||
make_wrapper() {
|
||||
this._super();
|
||||
|
|
|
|||
|
|
@ -439,6 +439,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
return this.script_manager.trigger("onload_post_render");
|
||||
}
|
||||
},
|
||||
() => this.run_after_load_hook(),
|
||||
() => this.dashboard.after_refresh()
|
||||
]);
|
||||
// focus on first input
|
||||
|
|
@ -462,6 +463,15 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
this.scroll_to_element();
|
||||
}
|
||||
|
||||
run_after_load_hook() {
|
||||
if (frappe.route_options.after_load) {
|
||||
let route_callback = frappe.route_options.after_load;
|
||||
delete frappe.route_options.after_load;
|
||||
|
||||
route_callback(this);
|
||||
}
|
||||
}
|
||||
|
||||
refresh_fields() {
|
||||
this.layout.refresh(this.doc);
|
||||
this.layout.primary_button = this.$wrapper.find(".btn-primary");
|
||||
|
|
@ -569,6 +579,13 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
|
||||
me.script_manager.trigger("after_save");
|
||||
|
||||
if (frappe.route_options.after_save) {
|
||||
let route_callback = frappe.route_options.after_save;
|
||||
delete frappe.route_options.after_save;
|
||||
|
||||
route_callback(me);
|
||||
}
|
||||
// submit comment if entered
|
||||
if (me.timeline) {
|
||||
me.timeline.comment_area.submit();
|
||||
|
|
@ -1530,7 +1547,7 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
|
||||
// scroll to input
|
||||
frappe.utils.scroll_to($el);
|
||||
frappe.utils.scroll_to($el, true, 15);
|
||||
|
||||
// highlight input
|
||||
$el.addClass('has-error');
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ frappe.quick_edit = function(doctype, name) {
|
|||
});
|
||||
};
|
||||
|
||||
frappe.ui.form.make_quick_entry = (doctype, after_insert, init_callback, doc) => {
|
||||
frappe.ui.form.make_quick_entry = (doctype, after_insert, init_callback, doc, force) => {
|
||||
var trimmed_doctype = doctype.replace(/ /g, '');
|
||||
var controller_name = "QuickEntryForm";
|
||||
|
||||
|
|
@ -14,31 +14,31 @@ frappe.ui.form.make_quick_entry = (doctype, after_insert, init_callback, doc) =>
|
|||
controller_name = trimmed_doctype + "QuickEntryForm";
|
||||
}
|
||||
|
||||
frappe.quick_entry = new frappe.ui.form[controller_name](doctype, after_insert, init_callback, doc);
|
||||
frappe.quick_entry = new frappe.ui.form[controller_name](doctype, after_insert, init_callback, doc, force);
|
||||
return frappe.quick_entry.setup();
|
||||
};
|
||||
|
||||
frappe.ui.form.QuickEntryForm = Class.extend({
|
||||
init: function(doctype, after_insert, init_callback, doc) {
|
||||
init: function(doctype, after_insert, init_callback, doc, force) {
|
||||
this.doctype = doctype;
|
||||
this.after_insert = after_insert;
|
||||
this.init_callback = init_callback;
|
||||
this.doc = doc;
|
||||
this.force = force ? force : false;
|
||||
},
|
||||
|
||||
setup: function() {
|
||||
let me = this;
|
||||
return new Promise(resolve => {
|
||||
frappe.model.with_doctype(this.doctype, function() {
|
||||
me.check_quick_entry_doc();
|
||||
me.set_meta_and_mandatory_fields();
|
||||
if(me.is_quick_entry()) {
|
||||
me.render_dialog();
|
||||
resolve(me);
|
||||
frappe.model.with_doctype(this.doctype, () => {
|
||||
this.check_quick_entry_doc();
|
||||
this.set_meta_and_mandatory_fields();
|
||||
if (this.is_quick_entry() || this.force) {
|
||||
this.render_dialog();
|
||||
resolve(this);
|
||||
} else {
|
||||
frappe.quick_entry = null;
|
||||
frappe.set_route('Form', me.doctype, me.doc.name)
|
||||
.then(() => resolve(me));
|
||||
frappe.set_route('Form', this.doctype, this.doc.name)
|
||||
.then(() => resolve(this));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -49,8 +49,8 @@ frappe.ui.form.QuickEntryForm = Class.extend({
|
|||
let fields = this.meta.fields;
|
||||
|
||||
// prepare a list of mandatory, bold and allow in quick entry fields
|
||||
this.mandatory = $.map(fields, function(d) {
|
||||
return ((d.reqd || d.bold || d.allow_in_quick_entry) && !d.read_only) ? $.extend({}, d) : null;
|
||||
this.mandatory = fields.filter(df => {
|
||||
return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -187,7 +187,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
|
|||
},
|
||||
error: function() {
|
||||
if (!me.skip_redirect_on_error) {
|
||||
me.open_doc();
|
||||
me.open_doc(true);
|
||||
}
|
||||
},
|
||||
always: function() {
|
||||
|
|
@ -245,9 +245,15 @@ frappe.ui.form.QuickEntryForm = Class.extend({
|
|||
return this.dialog.doc;
|
||||
},
|
||||
|
||||
open_doc: function(){
|
||||
open_doc: function(set_hooks) {
|
||||
this.dialog.hide();
|
||||
this.update_doc();
|
||||
if (set_hooks && this.after_insert) {
|
||||
frappe.route_options = frappe.route_options || {};
|
||||
frappe.route_options.after_save = (frm) => {
|
||||
this.after_insert(frm);
|
||||
};
|
||||
}
|
||||
frappe.set_route('Form', this.doctype, this.doc.name);
|
||||
},
|
||||
|
||||
|
|
@ -258,7 +264,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
|
|||
|
||||
$link.find('.edit-full').on('click', function() {
|
||||
// edit in form
|
||||
me.open_doc();
|
||||
me.open_doc(true);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ frappe.ui.form.Review = class Review {
|
|||
});
|
||||
}
|
||||
make_review_container() {
|
||||
this.parent.append(`
|
||||
<ul class="list-unstyled sidebar-menu">
|
||||
<li class="h6 reviews-label">${__('Reviews')}</li>
|
||||
<li class="review-list"></li>
|
||||
</ul>
|
||||
`);
|
||||
this.review_list_wrapper = this.parent.find('.review-list');
|
||||
}
|
||||
add_review_button() {
|
||||
|
|
|
|||
|
|
@ -69,10 +69,7 @@
|
|||
<div class="clearfix"></div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="list-unstyled sidebar-menu form-reviews">
|
||||
<li class="h6 attachments-label">{%= __("Reviews") %}</li>
|
||||
<li class="review-list"></li>
|
||||
</ul>
|
||||
<span class="form-reviews"></span>
|
||||
<ul class="list-unstyled sidebar-menu">
|
||||
<li class="h6 shared-with-label">{%= __("Shared With") %}</li>
|
||||
<li class="form-shared"></li>
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ frappe.views.BaseList = class BaseList {
|
|||
show_sidebar = !show_sidebar;
|
||||
localStorage.show_sidebar = show_sidebar;
|
||||
this.show_or_hide_sidebar();
|
||||
$(document.body).trigger('toggleListSidebar');
|
||||
}
|
||||
|
||||
show_or_hide_sidebar() {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
</a>
|
||||
<ul class="dropdown-menu list-stats-dropdown" role="menu">
|
||||
<div class="dropdown-search">
|
||||
<input type="text" placeholder="Search" class="form-control dropdown-search-input input-xs">
|
||||
<input type="text" placeholder="Search" data-element="search" class="form-control input-xs">
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -209,11 +209,13 @@ frappe.views.ListSidebar = class ListSidebar {
|
|||
accounts.forEach((account) => {
|
||||
let email_account = (account.email_id == "All Accounts") ? "All Accounts" : account.email_account;
|
||||
let route = ["List", "Communication", "Inbox", email_account].join('/');
|
||||
let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes(account.email_id) ? __(account.email_id) : account.email_id;
|
||||
|
||||
if (!divider) {
|
||||
this.get_divider().appendTo($dropdown);
|
||||
divider = true;
|
||||
}
|
||||
$(`<li><a href="#${route}">${account.email_id}</a></li>`).appendTo($dropdown);
|
||||
$(`<li><a href="#${route}">${display_name}</a></li>`).appendTo($dropdown);
|
||||
if (account.email_id === "Sent Mail")
|
||||
divider = false;
|
||||
});
|
||||
|
|
@ -240,40 +242,6 @@ frappe.views.ListSidebar = class ListSidebar {
|
|||
});
|
||||
}
|
||||
|
||||
setup_dropdown_search(dropdown, text_class) {
|
||||
let $dropdown_search = dropdown.find('.dropdown-search').show();
|
||||
let $search_input = $dropdown_search.find('.dropdown-search-input');
|
||||
$search_input.focus();
|
||||
$dropdown_search.on('click',(e)=>{
|
||||
e.stopPropagation();
|
||||
});
|
||||
let $elements = dropdown.find('li');
|
||||
$dropdown_search.on('keyup',()=> {
|
||||
let text_filter = $search_input.val().toLowerCase();
|
||||
// Replace trailing and leading spaces
|
||||
text_filter = text_filter.replace(/^\s+|\s+$/g, '');
|
||||
for (var i = 0; i < $elements.length; i++) {
|
||||
let text_element = $elements.eq(i).find(text_class);
|
||||
|
||||
let text = text_element.text().toLowerCase();
|
||||
// Search data-name since label for current user is 'Me'
|
||||
let name = '';
|
||||
if (text_element.data('name')) {
|
||||
name = text_element.data('name').toLowerCase();
|
||||
}
|
||||
if (text.includes(text_filter) || name.includes(text_filter)) {
|
||||
$elements.eq(i).css('display','');
|
||||
} else {
|
||||
$elements.eq(i).css('display','none');
|
||||
}
|
||||
}
|
||||
});
|
||||
dropdown.parent().on('hide.bs.dropdown',()=> {
|
||||
$dropdown_search.val('');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
get_cat_tags() {
|
||||
return this.cat_tags;
|
||||
}
|
||||
|
|
@ -292,7 +260,7 @@ frappe.views.ListSidebar = class ListSidebar {
|
|||
callback: function(r) {
|
||||
me.render_stat("_user_tags", (r.message.stats || {})["_user_tags"]);
|
||||
let stats_dropdown = me.sidebar.find('.list-stats-dropdown');
|
||||
me.setup_dropdown_search(stats_dropdown,'.stat-label');
|
||||
frappe.utils.setup_search(stats_dropdown, '.stat-link', '.stat-label');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ frappe.views.ListGroupBy = class ListGroupBy {
|
|||
title: __("Select Filters"),
|
||||
fields: this.get_group_by_dropdown_fields()
|
||||
});
|
||||
|
||||
d.set_primary_action("Save", ({ group_by_fields }) => {
|
||||
frappe.model.user_settings.save(this.doctype, 'group_by_fields', group_by_fields || null);
|
||||
this.group_by_fields = group_by_fields ? ['assigned_to', 'owner', ...group_by_fields] : ['assigned_to', 'owner'];
|
||||
|
|
@ -29,7 +30,12 @@ frappe.views.ListGroupBy = class ListGroupBy {
|
|||
d.hide();
|
||||
});
|
||||
|
||||
this.page.sidebar.find(".add-list-group-by a ").on("click", () => {
|
||||
d.$body.prepend(`<div class="filters-search">
|
||||
<input type="text" placeholder="${__('Search')}" data-element="search" class="form-control input-xs">
|
||||
</div>`);
|
||||
|
||||
this.page.sidebar.find(".add-list-group-by a").on("click", () => {
|
||||
frappe.utils.setup_search(d.$body, '.unit-checkbox', '.label-area');
|
||||
d.show();
|
||||
});
|
||||
}
|
||||
|
|
@ -95,7 +101,7 @@ frappe.views.ListGroupBy = class ListGroupBy {
|
|||
this.get_group_by_count(fieldname).then(field_count_list => {
|
||||
if (field_count_list.length) {
|
||||
this.render_dropdown_items(field_count_list, fieldtype, dropdown);
|
||||
this.sidebar.setup_dropdown_search(dropdown, '.group-by-value');
|
||||
frappe.utils.setup_search(dropdown, '.group-by-item', '.group-by-value', 'data-name');
|
||||
} else {
|
||||
dropdown.find('.group-by-loading').html(`${__("No filters found")}`);
|
||||
}
|
||||
|
|
@ -165,7 +171,7 @@ frappe.views.ListGroupBy = class ListGroupBy {
|
|||
};
|
||||
let standard_html = `
|
||||
<div class="dropdown-search">
|
||||
<input type="text" placeholder="${__('Search')}" class="form-control dropdown-search-input input-xs">
|
||||
<input type="text" placeholder="${__('Search')}" data-element="search" class="dropdown-search-input form-control input-xs">
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,26 +3,41 @@
|
|||
|
||||
// for translation
|
||||
frappe._messages = {};
|
||||
frappe._ = function(txt, replace) {
|
||||
if(!txt)
|
||||
return txt;
|
||||
if(typeof(txt) != "string")
|
||||
return txt;
|
||||
var ret = frappe._messages[txt.replace(/\n/g, "")] || txt;
|
||||
if(replace && typeof(replace) === "object") {
|
||||
ret = $.format(ret, replace);
|
||||
frappe._ = function(txt, replace, context = null) {
|
||||
if ($.isEmptyObject(frappe._messages) && frappe.boot) {
|
||||
$.extend(frappe._messages, frappe.boot.__messages);
|
||||
}
|
||||
return ret;
|
||||
if (!txt) return txt;
|
||||
if (typeof txt != "string") return txt;
|
||||
|
||||
let translated_text = '';
|
||||
|
||||
let key = txt.replace(/\n/g, "");
|
||||
if (context) {
|
||||
translated_text = frappe._messages[`${key}:${context}`];
|
||||
}
|
||||
|
||||
if (!translated_text) {
|
||||
translated_text = frappe._messages[key] || txt;
|
||||
}
|
||||
|
||||
if (replace && typeof replace === "object") {
|
||||
translated_text = $.format(translated_text, replace);
|
||||
}
|
||||
return translated_text;
|
||||
};
|
||||
window.__ = frappe._
|
||||
|
||||
window.__ = frappe._;
|
||||
|
||||
frappe.get_languages = function() {
|
||||
if(!frappe.languages) {
|
||||
frappe.languages = []
|
||||
$.each(frappe.boot.lang_dict, function(lang, value){
|
||||
frappe.languages.push({'label': lang, 'value': value})
|
||||
if (!frappe.languages) {
|
||||
frappe.languages = [];
|
||||
$.each(frappe.boot.lang_dict, function(lang, value) {
|
||||
frappe.languages.push({ label: lang, value: value });
|
||||
});
|
||||
frappe.languages = frappe.languages.sort(function(a, b) {
|
||||
return a.value < b.value ? -1 : 1;
|
||||
});
|
||||
frappe.languages = frappe.languages.sort(function(a, b) { return (a.value < b.value) ? -1 : 1 });
|
||||
}
|
||||
return frappe.languages;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -146,6 +146,12 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
|
|||
this.get_close_btn().on('click', click);
|
||||
}
|
||||
|
||||
set_secondary_action_label(label) {
|
||||
this.get_close_btn()
|
||||
.removeClass("hide")
|
||||
.html(label);
|
||||
}
|
||||
|
||||
disable_primary_action() {
|
||||
this.get_primary_btn().addClass('disabled');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,6 +169,11 @@ frappe.msgprint = function(msg, title, is_minimizable) {
|
|||
}
|
||||
}
|
||||
|
||||
if (data.secondary_action) {
|
||||
frappe.msg_dialog.set_secondary_action(data.secondary_action.action);
|
||||
frappe.msg_dialog.set_secondary_action_label(__(data.secondary_action.label || "Close"));
|
||||
}
|
||||
|
||||
if(data.message==null) {
|
||||
data.message = '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,179 +0,0 @@
|
|||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.provide("frappe.setup");
|
||||
frappe.provide("frappe.ui");
|
||||
|
||||
frappe.setup.OnboardingSlide = class OnboardingSlide extends frappe.ui.Slide {
|
||||
constructor(slide = null) {
|
||||
super(slide);
|
||||
}
|
||||
|
||||
make() {
|
||||
super.make();
|
||||
this.$next_btn = this.slides_footer.find('.next-btn');
|
||||
this.$complete_btn = this.slides_footer.find('.complete-btn');
|
||||
this.$action_button = this.slides_footer.find('.next-btn');
|
||||
|
||||
if (this.help_links) {
|
||||
this.$help_links = $(`<div class="text-center">
|
||||
<div class="help-links"></div>
|
||||
</div>`).appendTo(this.$body);
|
||||
this.setup_help_links();
|
||||
}
|
||||
|
||||
this.$skip_btn = this.slides_footer.find('.skip-btn').on('click', () => {
|
||||
$('.onboarding-dialog').modal('toggle');
|
||||
});
|
||||
}
|
||||
|
||||
setup_form() {
|
||||
super.setup_form();
|
||||
const fields = this.get_atomic_fields();
|
||||
|
||||
// remove link indicator
|
||||
fields.map((field) => {
|
||||
if (field.fieldtype == 'Link') {
|
||||
$('.link-btn').remove();
|
||||
}
|
||||
});
|
||||
|
||||
if (fields.length == 1) {
|
||||
this.$form_wrapper.addClass("text-center");
|
||||
} else {
|
||||
this.$form_wrapper.removeClass("text-center");
|
||||
}
|
||||
}
|
||||
|
||||
before_show() {
|
||||
if (this.id === 0) {
|
||||
this.$next_btn.text(__('Let\'s Go'));
|
||||
this.$skip_btn.removeClass('hide');
|
||||
} else {
|
||||
this.$next_btn.text(__('Next'));
|
||||
this.$skip_btn.addClass('hide');
|
||||
}
|
||||
//last slide
|
||||
if (this.is_last_slide()) {
|
||||
this.$complete_btn.removeClass('hide').addClass('action primary');
|
||||
this.$next_btn.removeClass('action primary');
|
||||
this.$action_button = this.$complete_btn;
|
||||
}
|
||||
this.setup_action_button();
|
||||
}
|
||||
|
||||
primary_action() {
|
||||
if (this.set_values()) {
|
||||
this.$action_button.addClass('disabled');
|
||||
const primary_method = 'frappe.desk.doctype.onboarding_slide.onboarding_slide.create_onboarding_docs';
|
||||
if (this.add_more) {
|
||||
this.values.max_count = this.max_count;
|
||||
}
|
||||
frappe.call(primary_method, {
|
||||
values: this.values,
|
||||
doctype: this.ref_doctype,
|
||||
app: this.app,
|
||||
slide_type: this.slide_type
|
||||
}).then(() => {
|
||||
if (this.is_last_slide()) {
|
||||
this.reset_is_first_startup();
|
||||
$('.onboarding-dialog').modal('toggle');
|
||||
frappe.msgprint({
|
||||
message: __('You are all set up!'),
|
||||
indicator: 'green',
|
||||
title: __('Success')
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unbind_primary_action() {
|
||||
// unbind only action method as next button is same as create button in this setup wizard
|
||||
this.$action_button.off('click.primary_action');
|
||||
}
|
||||
|
||||
setup_help_links() {
|
||||
this.help_links.map(link => {
|
||||
let $link = $(
|
||||
`<a target="_blank" class="small text-muted">${link.label || __("Need Help?")}</a>`
|
||||
);
|
||||
if (link.video_id) {
|
||||
$link.on('click', () => {
|
||||
frappe.help.show_video(link.video_id, link.label);
|
||||
});
|
||||
}
|
||||
this.$help_links.append($link);
|
||||
});
|
||||
}
|
||||
|
||||
setup_action_button() {
|
||||
if (this.slide_type === 'Create' || this.slide_type == 'Settings' || this.is_last_slide()) {
|
||||
this.$action_button.addClass('primary');
|
||||
} else {
|
||||
this.$action_button.removeClass('primary');
|
||||
}
|
||||
|
||||
this.$action_button.on('click', () => {
|
||||
if (this.slide_type != 'Continue') {
|
||||
this.mark_as_completed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mark_as_completed() {
|
||||
frappe.call({
|
||||
method: 'frappe.desk.doctype.onboarding_slide.onboarding_slide.mark_slide_as_completed',
|
||||
args: {slide_title: this.title},
|
||||
callback: () => {},
|
||||
freeze: true
|
||||
});
|
||||
}
|
||||
|
||||
reset_is_first_startup() {
|
||||
frappe.call({
|
||||
method: "frappe.desk.page.setup_wizard.setup_wizard.reset_is_first_startup",
|
||||
args: {},
|
||||
callback: () => {}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
frappe.setup.OnboardingDialog = class OnboardingDialog {
|
||||
constructor({
|
||||
slides = []
|
||||
}) {
|
||||
this.slides = slides;
|
||||
this.setup();
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
static: true,
|
||||
minimizable: false,
|
||||
});
|
||||
this.$wrapper = $(this.dialog.$wrapper).addClass('onboarding-dialog');
|
||||
this.slide_container = new frappe.ui.Slides({
|
||||
parent: this.dialog.body,
|
||||
slides: this.slides,
|
||||
slide_class: frappe.setup.OnboardingSlide,
|
||||
unidirectional: 1,
|
||||
before_load: ($footer) => {
|
||||
$footer.find('.prev-btn').addClass('hide');
|
||||
$footer.find('.next-btn').removeClass('btn-default').addClass('btn-primary action');
|
||||
$footer.find('.prev-div').prepend(
|
||||
$(`<a class="skip-btn text-muted btn btn-link btn-sm hide">
|
||||
${__("Do It Later")}</a>`));
|
||||
$footer.find('.next-div').prepend(
|
||||
$(`<a class="complete-btn btn btn-primary btn-sm hide">
|
||||
${__("Complete")}</a>`));
|
||||
}
|
||||
});
|
||||
|
||||
this.$wrapper.find('.modal-header').remove();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.dialog.show();
|
||||
}
|
||||
};
|
||||
|
|
@ -16,6 +16,11 @@ frappe.help.show = function(doctype) {
|
|||
}
|
||||
|
||||
frappe.help.show_video = function(youtube_id, title) {
|
||||
if (frappe.utils.is_url(youtube_id)) {
|
||||
const expression = '(?:youtube.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu.be/)([^\"&?/s]{11})';
|
||||
youtube_id = youtube_id.match(expression)[1];
|
||||
}
|
||||
|
||||
if($("body").width() > 768) {
|
||||
var size = [670, 377];
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -695,7 +695,32 @@ Object.assign(frappe.utils, {
|
|||
return null;
|
||||
}
|
||||
},
|
||||
setup_search($wrapper, el_class, text_class, data_attr) {
|
||||
const $search_input = $wrapper.find('[data-element="search"]').show();
|
||||
$search_input.focus().val('');
|
||||
const $elements = $wrapper.find(el_class).show();
|
||||
|
||||
$search_input.off('keyup').on('keyup', () => {
|
||||
let text_filter = $search_input.val().toLowerCase();
|
||||
// Replace trailing and leading spaces
|
||||
text_filter = text_filter.replace(/^\s+|\s+$/g, '');
|
||||
for (let i = 0; i < $elements.length; i++) {
|
||||
const text_element = $elements.eq(i).find(text_class);
|
||||
const text = text_element.text().toLowerCase();
|
||||
|
||||
let name = '';
|
||||
if (data_attr && text_element.attr(data_attr)) {
|
||||
name = text_element.attr(data_attr).toLowerCase();
|
||||
}
|
||||
|
||||
if (text.includes(text_filter) || name.includes(text_filter)) {
|
||||
$elements.eq(i).css('display', '');
|
||||
} else {
|
||||
$elements.eq(i).css('display', 'none');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
deep_equal(a, b) {
|
||||
return deep_equal(a, b);
|
||||
},
|
||||
|
|
@ -777,7 +802,7 @@ Object.assign(frappe.utils, {
|
|||
name: M[0],
|
||||
version: M[1],
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
// Array de duplicate
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
this.setup_dashboard_page();
|
||||
this.setup_dashboard_customization();
|
||||
this.make_dashboard();
|
||||
this.setup_events();
|
||||
}
|
||||
|
||||
setup_dashboard_customization() {
|
||||
|
|
@ -43,13 +44,11 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
<div class="text-muted uppercase">${dashboard_name}</div>
|
||||
<div class="text-muted customize-dashboard" data-action="customize">${__('Customize')}</div>
|
||||
<div class="small text-muted customize-options small-bounce">
|
||||
<span class="reset-customization" data-action="reset_dashboard_customization">
|
||||
${__('Reset')}
|
||||
</span> / <span class="save-customization" data-action="save_dashboard_customization">
|
||||
${__('Save')}
|
||||
</span> / <span class="discard-customization" data-action="discard_dashboard_customization">
|
||||
${__('Discard')}
|
||||
</span>
|
||||
<span class="reset-customization customize-option" data-action="reset_dashboard_customization">${__('Reset')}</span>
|
||||
<span> / </span>
|
||||
<span class="save-customization customize-option" data-action="save_dashboard_customization">${__('Save')}</span>
|
||||
<span> / </span>
|
||||
<span class="discard-customization customize-option" data-action="discard_dashboard_customization">${__('Discard')}</span>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
|
|
@ -105,6 +104,11 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
}
|
||||
}
|
||||
|
||||
setup_events() {
|
||||
$(document.body).on('toggleFullWidth', () => this.render_dashboard());
|
||||
$(document.body).on('toggleListSidebar', () => this.render_dashboard());
|
||||
}
|
||||
|
||||
fetch_dashboard_items(doctype, filters, obj_name) {
|
||||
return frappe.db.get_list(doctype, {
|
||||
filters: filters,
|
||||
|
|
@ -188,9 +192,8 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
}
|
||||
|
||||
this.toggle_customize(true);
|
||||
this.chart_group.in_customize_mode = true;
|
||||
this.in_customize_mode = true;
|
||||
this.chart_group.customize();
|
||||
this.number_cards.in_customize_mode = true;
|
||||
this.number_card_group.customize();
|
||||
}
|
||||
|
||||
|
|
@ -225,12 +228,14 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
|
|||
}
|
||||
|
||||
reset_dashboard_customization() {
|
||||
this.dashboard_settings = null;
|
||||
frappe.model.user_settings.save(
|
||||
this.doctype, 'dashboard_settings', this.dashboard_settings
|
||||
).then(() => this.make_dashboard());
|
||||
frappe.confirm(__("Are you sure you want to reset all customizations?"), () => {
|
||||
this.dashboard_settings = null;
|
||||
frappe.model.user_settings.save(
|
||||
this.doctype, 'dashboard_settings', this.dashboard_settings
|
||||
).then(() => this.make_dashboard());
|
||||
|
||||
this.toggle_customize(false);
|
||||
this.toggle_customize(false);
|
||||
});
|
||||
}
|
||||
|
||||
toggle_customize(show) {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue