Merge branch 'develop' into setup-search
This commit is contained in:
commit
5c8dde1deb
332 changed files with 260031 additions and 256909 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
|
||||
13
.travis.yml
13
.travis.yml
|
|
@ -41,11 +41,6 @@ matrix:
|
|||
- 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
|
||||
|
|
@ -59,10 +54,9 @@ before_install:
|
|||
install:
|
||||
- cd ~
|
||||
- source ./.nvm/nvm.sh
|
||||
- nvm install v8.10.0
|
||||
- nvm install 12
|
||||
|
||||
- git clone https://github.com/frappe/bench --depth 1
|
||||
- pip install -e ./bench
|
||||
- pip install frappe-bench
|
||||
|
||||
- bench init frappe-bench --skip-assets --python $(which python) --frappe-path $TRAVIS_BUILD_DIR
|
||||
|
||||
|
|
@ -104,8 +98,7 @@ install:
|
|||
- if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi
|
||||
- if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi
|
||||
|
||||
- if [ $TYPE == "ui" ]; then bench setup requirements --node; fi
|
||||
|
||||
- bench setup requirements --node
|
||||
- bench start &
|
||||
- bench --site test_site reinstall --yes
|
||||
- bench --site test_site_producer reinstall --yes
|
||||
|
|
|
|||
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
|
||||
|
|
|
|||
|
|
@ -44,4 +44,21 @@ context('Form', () => {
|
|||
list_view.filter_area.filter_list.clear_filters();
|
||||
});
|
||||
});
|
||||
it('validates behaviour of Data options validations in child table', () => {
|
||||
// test email validations for set_invalid controller
|
||||
let website_input = 'website.in';
|
||||
let expectBackgroundColor = 'rgb(255, 220, 220)';
|
||||
|
||||
cy.visit('/desk#Form/Contact/New Contact 1');
|
||||
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('.grid-body .rows [data-fieldname="email_id"]').click();
|
||||
cy.get('@table').find('input.input-with-feedback.form-control').as('email_input');
|
||||
cy.get('@email_input').type(website_input, { waitForAnimations: false });
|
||||
cy.fill_field('company_name', 'Test Company');
|
||||
cy.get('@email_input').should($div => {
|
||||
const style = window.getComputedStyle($div[0]);
|
||||
expect(style.backgroundColor).to.equal(expectBackgroundColor);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'''
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ global_cache_keys = ("app_hooks", "installed_apps",
|
|||
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
|
||||
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
|
||||
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts',
|
||||
'sitemap_routes')
|
||||
'sitemap_routes', 'db_tables')
|
||||
|
||||
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
|
||||
"defaults", "user_permissions", "home_page", "linked_with",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Users",
|
||||
"modified": "2020-04-01 11:24:40.767676",
|
||||
"modified": "2020-04-26 22:36:14.311554",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Users",
|
||||
|
|
@ -46,12 +46,12 @@
|
|||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "permission-manager",
|
||||
"label": "Permission Manager",
|
||||
"link_to": "permission-manager",
|
||||
"type": "Page"
|
||||
},
|
||||
{
|
||||
"label": "user-profile",
|
||||
"label": "User Profile",
|
||||
"link_to": "user-profile",
|
||||
"type": "Page"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]],
|
||||
|
|
|
|||
|
|
@ -71,7 +71,11 @@ class Exporter:
|
|||
return parent_fields
|
||||
|
||||
def get_exportable_children_fields(self):
|
||||
children = [df.options for df in self.meta.fields if df.fieldtype in table_fields]
|
||||
child_table_fields = [df for df in self.meta.fields if df.fieldtype in table_fields]
|
||||
if self.export_fields == "Mandatory":
|
||||
child_table_fields = [df for df in child_table_fields if df.reqd]
|
||||
|
||||
children = [df.options for df in child_table_fields]
|
||||
children_fields = []
|
||||
for child in children:
|
||||
children_fields += self.get_exportable_fields(child)
|
||||
|
|
|
|||
|
|
@ -351,9 +351,9 @@ class Importer:
|
|||
value = cstr(value)
|
||||
|
||||
# convert boolean values to 0 or 1
|
||||
if df.fieldtype == "Check" and value.lower().strip() in ["t", "f", "true", "false"]:
|
||||
if df.fieldtype == "Check" and value.lower().strip() in ["t", "f", "true", "false", "yes", "no", "y", "n"]:
|
||||
value = value.lower().strip()
|
||||
value = 1 if value in ["t", "true"] else 0
|
||||
value = 1 if value in ["t", "true", "y", "yes"] else 0
|
||||
|
||||
if df.fieldtype in ["Int", "Check"]:
|
||||
value = cint(value)
|
||||
|
|
@ -610,7 +610,7 @@ class Importer:
|
|||
"message": msg,
|
||||
}
|
||||
)
|
||||
return False
|
||||
return
|
||||
|
||||
elif df.fieldtype == "Link":
|
||||
d = self.get_missing_link_field_values(df.options)
|
||||
|
|
@ -643,8 +643,10 @@ class Importer:
|
|||
if value in INVALID_VALUES:
|
||||
value = None
|
||||
|
||||
value = validate_value(value, df)
|
||||
if value:
|
||||
if value is not None:
|
||||
value = validate_value(value, df)
|
||||
|
||||
if value is not None:
|
||||
doc[df.fieldname] = self.parse_value(value, df)
|
||||
|
||||
is_table = frappe.get_meta(doctype).istable
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class TestExporter(unittest.TestCase):
|
|||
e = Exporter('Web Page', export_fields='All')
|
||||
csv_array = e.get_csv_array()
|
||||
header = csv_array[0]
|
||||
self.assertEqual(len(header), 28)
|
||||
self.assertEqual(len(header), 36)
|
||||
|
||||
|
||||
def test_exports_selected_fields(self):
|
||||
|
|
|
|||
|
|
@ -337,7 +337,12 @@ frappe.ui.form.on('Data Import Beta', {
|
|||
let message = warnings_by_row[row_number]
|
||||
.map(w => {
|
||||
if (w.field) {
|
||||
return `<li>${w.field.label}: ${w.message}</li>`;
|
||||
let label =
|
||||
w.field.label +
|
||||
(w.field.parent !== frm.doc.reference_doctype
|
||||
? ` (${w.field.parent})`
|
||||
: '');
|
||||
return `<li>${label}: ${w.message}</li>`;
|
||||
}
|
||||
return `<li>${w.message}</li>`;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -712,9 +712,10 @@ def validate_fields(meta):
|
|||
if d.fieldtype == "Currency" and cint(d.width) < 100:
|
||||
frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx))
|
||||
|
||||
def check_in_list_view(d):
|
||||
def check_in_list_view(is_table, d):
|
||||
if d.in_list_view and (d.fieldtype in not_allowed_in_list_view):
|
||||
frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx))
|
||||
property_label = 'In Grid View' if is_table else 'In List View'
|
||||
frappe.throw(_("'{0}' not allowed for type {1} in row {2}").format(property_label, d.fieldtype, d.idx))
|
||||
|
||||
def check_in_global_search(d):
|
||||
if d.in_global_search and d.fieldtype in no_value_fields:
|
||||
|
|
@ -733,8 +734,11 @@ def validate_fields(meta):
|
|||
d.default = '0'
|
||||
if d.fieldtype == "Check" and d.default not in ('0', '1'):
|
||||
frappe.throw(_("Default for 'Check' type of field must be either '0' or '1'"))
|
||||
if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")):
|
||||
frappe.throw(_("Default for {0} must be an option").format(d.fieldname))
|
||||
if d.fieldtype == "Select" and d.default:
|
||||
if not d.options:
|
||||
frappe.throw(_("Options for {0} must be set before setting the default value.").format(frappe.bold(d.fieldname)))
|
||||
elif d.default not in d.options.split("\n"):
|
||||
frappe.throw(_("Default value for {0} must be in the list of options.").format(frappe.bold(d.fieldname)))
|
||||
|
||||
def check_precision(d):
|
||||
if d.fieldtype in ("Currency", "Float", "Percent") and d.precision is not None and not (1 <= cint(d.precision) <= 6):
|
||||
|
|
@ -903,6 +907,16 @@ def validate_fields(meta):
|
|||
|
||||
frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
|
||||
|
||||
def check_child_table_option(docfield):
|
||||
if docfield.fieldtype not in ['Table MultiSelect', 'Table']: return
|
||||
|
||||
doctype = docfield.options
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
if not meta.istable:
|
||||
frappe.throw(_('Option {0} for field {1} is not a child table')
|
||||
.format(frappe.bold(doctype), frappe.bold(docfield.fieldname)), title=_("Invalid Option"))
|
||||
|
||||
|
||||
fields = meta.get("fields")
|
||||
fieldname_list = [d.fieldname for d in fields]
|
||||
|
|
@ -926,11 +940,12 @@ def validate_fields(meta):
|
|||
check_link_table_options(meta.get("name"), d)
|
||||
check_dynamic_link_options(d)
|
||||
check_hidden_and_mandatory(meta.get("name"), d)
|
||||
check_in_list_view(d)
|
||||
check_in_list_view(meta.get('istable'), d)
|
||||
check_in_global_search(d)
|
||||
check_illegal_default(d)
|
||||
check_unique_and_text(meta.get("name"), d)
|
||||
check_illegal_depends_on_conditions(d)
|
||||
check_child_table_option(d)
|
||||
check_table_multiselect_option(d)
|
||||
scrub_options_in_select(d)
|
||||
scrub_fetch_from(d)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -76,7 +76,16 @@ class Dashboard {
|
|||
}
|
||||
|
||||
refresh() {
|
||||
this.get_permitted_dashboard_charts().then(charts => {
|
||||
frappe.run_serially([
|
||||
() => this.render_cards(),
|
||||
() => this.render_charts()
|
||||
]);
|
||||
}
|
||||
|
||||
render_charts() {
|
||||
return this.get_permitted_items(
|
||||
'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts'
|
||||
).then(charts => {
|
||||
if (!charts.length) {
|
||||
frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts'))
|
||||
}
|
||||
|
|
@ -92,6 +101,7 @@ class Dashboard {
|
|||
...chart
|
||||
}
|
||||
});
|
||||
|
||||
this.chart_group = new frappe.widget.WidgetGroup({
|
||||
title: null,
|
||||
container: this.container,
|
||||
|
|
@ -110,14 +120,46 @@ class Dashboard {
|
|||
});
|
||||
}
|
||||
|
||||
get_permitted_dashboard_charts() {
|
||||
render_cards() {
|
||||
return this.get_permitted_items(
|
||||
'frappe.desk.doctype.dashboard.dashboard.get_permitted_cards'
|
||||
).then(cards => {
|
||||
if (!cards.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.number_cards =
|
||||
cards.map(card => {
|
||||
return {
|
||||
name: card.card,
|
||||
};
|
||||
});
|
||||
|
||||
this.number_card_group = new frappe.widget.WidgetGroup({
|
||||
container: this.container,
|
||||
type: "number_card",
|
||||
columns: 3,
|
||||
options: {
|
||||
allow_sorting: false,
|
||||
allow_create: false,
|
||||
allow_delete: false,
|
||||
allow_hiding: false,
|
||||
allow_edit: false,
|
||||
},
|
||||
widgets: this.number_cards,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get_permitted_items(method) {
|
||||
return frappe.xcall(
|
||||
'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts',
|
||||
method,
|
||||
{
|
||||
dashboard_name: this.dashboard_name
|
||||
}).then(charts => {
|
||||
return charts;
|
||||
});
|
||||
}
|
||||
).then(items => {
|
||||
return items;
|
||||
});
|
||||
}
|
||||
|
||||
set_dropdown() {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ frappe.PermissionEngine = Class.extend({
|
|||
setup_page: function() {
|
||||
var me = this;
|
||||
this.doctype_select
|
||||
= this.wrapper.page.add_select(__("Document Types"),
|
||||
= this.wrapper.page.add_select(__("Document Type"),
|
||||
[{value: "", label: __("Select Document Type")+"..."}].concat(this.options.doctypes))
|
||||
.change(function() {
|
||||
frappe.set_route("permission-manager", $(this).val());
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -124,6 +124,8 @@ class Database(object):
|
|||
# in transaction validations
|
||||
self.check_transaction_status(query)
|
||||
|
||||
self.clear_db_table_cache(query)
|
||||
|
||||
# autocommit
|
||||
if auto_commit: self.commit()
|
||||
|
||||
|
|
@ -277,6 +279,11 @@ class Database(object):
|
|||
ret.append(frappe._dict(zip(keys, values)))
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def clear_db_table_cache(query):
|
||||
if query and query.strip().split()[0].lower() in {'drop', 'create'}:
|
||||
frappe.cache().delete_key('db_tables')
|
||||
|
||||
@staticmethod
|
||||
def needs_formatting(result, formatted):
|
||||
"""Returns true if the first row in the result has a Date, Datetime, Long Int."""
|
||||
|
|
@ -769,7 +776,16 @@ class Database(object):
|
|||
return ("tab" + doctype) in self.get_tables()
|
||||
|
||||
def get_tables(self):
|
||||
return [d[0] for d in self.sql("select table_name from information_schema.tables where table_schema not in ('pg_catalog', 'information_schema')")]
|
||||
tables = frappe.cache().get_value('db_tables')
|
||||
if not tables:
|
||||
table_rows = self.sql("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||
""")
|
||||
tables = {d[0] for d in table_rows}
|
||||
frappe.cache().set_value('db_tables', tables)
|
||||
return tables
|
||||
|
||||
def a_row_exists(self, doctype):
|
||||
"""Returns True if atleast one row exists."""
|
||||
|
|
|
|||
|
|
@ -137,16 +137,14 @@ class DBTable:
|
|||
if frappe.db.is_missing_column(e):
|
||||
# Unknown column 'column_name' in 'field list'
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
raise
|
||||
|
||||
if max_length and max_length[0][0] and max_length[0][0] > new_length:
|
||||
if col.fieldname in self.columns:
|
||||
self.columns[col.fieldname].length = current_length
|
||||
|
||||
frappe.msgprint(_("""Reverting length to {0} for '{1}' in '{2}';
|
||||
Setting the length as {3} will cause truncation of data.""")
|
||||
.format(current_length, col.fieldname, self.doctype, new_length))
|
||||
info_message = _("Reverting length to {0} for '{1}' in '{2}'. Setting the length as {3} will cause truncation of data.") \
|
||||
.format(current_length, col.fieldname, self.doctype, new_length)
|
||||
frappe.msgprint(info_message)
|
||||
|
||||
def is_new(self):
|
||||
return self.table_name not in frappe.db.get_tables()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -4,5 +4,21 @@
|
|||
frappe.ui.form.on('Dashboard', {
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name));
|
||||
|
||||
frm.set_query("chart", "charts", function() {
|
||||
return {
|
||||
filters: {
|
||||
is_public: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("card", "cards", function() {
|
||||
return {
|
||||
filters: {
|
||||
is_public: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
"field_order": [
|
||||
"dashboard_name",
|
||||
"is_default",
|
||||
"charts"
|
||||
"charts",
|
||||
"cards"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -31,10 +32,16 @@
|
|||
"label": "Charts",
|
||||
"options": "Dashboard Chart Link",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cards",
|
||||
"fieldtype": "Table",
|
||||
"label": "Cards",
|
||||
"options": "Number Card Link"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-03-25 21:09:37.080132",
|
||||
"modified": "2020-04-19 17:44:36.237163",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard",
|
||||
|
|
|
|||
|
|
@ -21,3 +21,12 @@ def get_permitted_charts(dashboard_name):
|
|||
if frappe.has_permission('Dashboard Chart', doc=chart.chart):
|
||||
permitted_charts.append(chart)
|
||||
return permitted_charts
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_permitted_cards(dashboard_name):
|
||||
permitted_cards = []
|
||||
dashboard = frappe.get_doc('Dashboard', dashboard_name)
|
||||
for card in dashboard.cards:
|
||||
if frappe.has_permission('Number Card', doc=card.card):
|
||||
permitted_cards.append(card)
|
||||
return permitted_cards
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
frm.add_fetch('source', 'timeseries', 'timeseries');
|
||||
},
|
||||
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.chart_filters = null;
|
||||
frm.add_custom_button('Add Chart to Dashboard', () => {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"aggregate_function_based_on",
|
||||
"number_of_groups",
|
||||
"column_break_6",
|
||||
"is_public",
|
||||
"timespan",
|
||||
"from_date",
|
||||
"to_date",
|
||||
|
|
@ -99,7 +100,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.chart_type !== 'Group By'",
|
||||
"depends_on": "eval: ['Count', 'Sum', 'Average'].includes(doc.chart_type)",
|
||||
"fieldname": "timeseries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Time Series"
|
||||
|
|
@ -220,10 +221,17 @@
|
|||
"fieldname": "custom_options",
|
||||
"fieldtype": "Code",
|
||||
"label": "Custom Options"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "This chart will be available to all Users if this is set",
|
||||
"fieldname": "is_public",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Public"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-04-20 23:49:11.389909",
|
||||
"modified": "2020-05-01 15:22:59.119341",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard Chart",
|
||||
|
|
@ -254,6 +262,7 @@
|
|||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
|
@ -92,20 +92,26 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
|
|||
return chart_config
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_report_chart(args):
|
||||
def create_dashboard_chart(args):
|
||||
args = frappe.parse_json(args)
|
||||
_doc = frappe.new_doc('Dashboard Chart')
|
||||
doc = frappe.new_doc('Dashboard Chart')
|
||||
|
||||
_doc.update(args)
|
||||
doc.update(args)
|
||||
|
||||
if (args.get("custom_options")):
|
||||
_doc.custom_options = json.dumps(args.get("custom_options"))
|
||||
if args.get('custom_options'):
|
||||
doc.custom_options = json.dumps(args.get('custom_options'))
|
||||
|
||||
if frappe.db.exists('Dashboard Chart', args.chart_name):
|
||||
args.chart_name = append_number_if_name_exists('Dashboard Chart', args.chart_name)
|
||||
_doc.chart_name = args.chart_name
|
||||
_doc.insert(ignore_permissions=True)
|
||||
doc.chart_name = args.chart_name
|
||||
doc.insert(ignore_permissions=True)
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_report_chart(args):
|
||||
create_dashboard_chart(args)
|
||||
args = frappe.parse_json(args)
|
||||
if args.dashboard:
|
||||
add_chart_to_dashboard(json.dumps(args))
|
||||
|
||||
|
|
@ -356,6 +362,13 @@ def get_year_ending(date):
|
|||
# last day of this month
|
||||
return add_to_date(date, days=-1)
|
||||
|
||||
def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
|
||||
or_filters = {'owner': frappe.session.user, 'is_public': 1}
|
||||
return frappe.db.get_list('Dashboard Chart',
|
||||
fields=['name'],
|
||||
filters=filters,
|
||||
or_filters=or_filters,
|
||||
as_list = 1)
|
||||
|
||||
class DashboardChart(Document):
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
119
frappe/desk/doctype/number_card/number_card.js
Normal file
119
frappe/desk/doctype/number_card/number_card.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Number Card', {
|
||||
refresh: function(frm) {
|
||||
frm.set_df_property("filters_section", "hidden", 1);
|
||||
frm.trigger('set_options');
|
||||
frm.trigger('render_filters_table');
|
||||
},
|
||||
|
||||
document_type: function(frm) {
|
||||
frm.set_query('document_type', function() {
|
||||
return {
|
||||
filters: {
|
||||
'issingle': false
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_value('filters_json', '[]');
|
||||
frm.set_value('aggregate_function_based_on', '');
|
||||
frm.trigger('set_options');
|
||||
},
|
||||
|
||||
set_options: function(frm) {
|
||||
let aggregate_based_on_fields = [];
|
||||
const doctype = frm.doc.document_type;
|
||||
|
||||
if (doctype) {
|
||||
frappe.model.with_doctype(doctype, () => {
|
||||
frappe.get_meta(doctype).fields.map(df => {
|
||||
if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) {
|
||||
if (df.fieldtype == 'Currency') {
|
||||
if (!df.options || df.options !== 'Company:company:default_currency') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
aggregate_based_on_fields.push({label: df.label, value: df.fieldname});
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render_filters_table: function(frm) {
|
||||
frm.set_df_property("filters_section", "hidden", 0);
|
||||
|
||||
let wrapper = $(frm.get_field('filters_json').wrapper).empty();
|
||||
frm.filter_table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 33%">${__('Filter')}</th>
|
||||
<th style="width: 33%">${__('Condition')}</th>
|
||||
<th>${__('Value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>`).appendTo(wrapper);
|
||||
|
||||
frm.filters = JSON.parse(frm.doc.filters_json || '[]');
|
||||
|
||||
frm.trigger('set_filters_in_table');
|
||||
|
||||
frm.filter_table.on('click', () => {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Set Filters'),
|
||||
fields: [{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'filter_area',
|
||||
}],
|
||||
primary_action: function() {
|
||||
let values = this.get_values();
|
||||
if (values) {
|
||||
this.hide();
|
||||
frm.filters = frm.filter_group.get_filters();
|
||||
frm.set_value('filters_json', JSON.stringify(frm.filters));
|
||||
frm.trigger('set_filters_in_table');
|
||||
}
|
||||
},
|
||||
primary_action_label: "Set"
|
||||
});
|
||||
|
||||
frappe.dashboards.filters_dialog = dialog;
|
||||
|
||||
frm.filter_group = new frappe.ui.FilterGroup({
|
||||
parent: dialog.get_field('filter_area').$wrapper,
|
||||
doctype: frm.doc.document_type,
|
||||
on_change: () => {},
|
||||
});
|
||||
|
||||
frm.filter_group.add_filters_to_filter_group(frm.filters);
|
||||
|
||||
dialog.show();
|
||||
dialog.set_values(frm.filters);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
set_filters_in_table: function(frm) {
|
||||
if (!frm.filters.length) {
|
||||
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
|
||||
${__("Click to Set Filters")}</td></tr>`);
|
||||
frm.filter_table.find('tbody').html(filter_row);
|
||||
} else {
|
||||
let filter_rows = '';
|
||||
frm.filters.forEach(filter => {
|
||||
filter_rows +=
|
||||
`<tr>
|
||||
<td>${filter[1]}</td>
|
||||
<td>${filter[2] || ""}</td>
|
||||
<td>${filter[3]}</td>
|
||||
</tr>`;
|
||||
|
||||
});
|
||||
frm.filter_table.find('tbody').html(filter_rows);
|
||||
}
|
||||
}
|
||||
});
|
||||
147
frappe/desk/doctype/number_card/number_card.json
Normal file
147
frappe/desk/doctype/number_card/number_card.json
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "CARD.#####",
|
||||
"creation": "2020-04-15 18:06:39.444683",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"label",
|
||||
"function",
|
||||
"aggregate_function_based_on",
|
||||
"column_break_2",
|
||||
"document_type",
|
||||
"is_public",
|
||||
"stats_section",
|
||||
"show_percentage_stats",
|
||||
"stats_time_interval",
|
||||
"filters_section",
|
||||
"filters_json",
|
||||
"color"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "document_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.document_type",
|
||||
"fieldname": "function",
|
||||
"fieldtype": "Select",
|
||||
"label": "Function",
|
||||
"options": "Count\nSum\nAverage\nMinimum\nMaximum",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.function !== 'Count'",
|
||||
"fieldname": "aggregate_function_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Aggregate Function Based On",
|
||||
"mandatory_depends_on": "eval: doc.function !== 'Count'"
|
||||
},
|
||||
{
|
||||
"fieldname": "filters_json",
|
||||
"fieldtype": "Code",
|
||||
"label": "Filters JSON",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Label",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "color",
|
||||
"fieldtype": "Color",
|
||||
"label": "Color"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "filters_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Filters Section"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "This card will be available to all Users if this is set",
|
||||
"fieldname": "is_public",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Public"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "show_percentage_stats",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Percentage Stats"
|
||||
},
|
||||
{
|
||||
"default": "Daily",
|
||||
"depends_on": "eval: doc.show_percentage_stats",
|
||||
"description": "Show percentage difference according to this time interval",
|
||||
"fieldname": "stats_time_interval",
|
||||
"fieldtype": "Select",
|
||||
"label": "Stats Time Interval",
|
||||
"options": "Daily\nWeekly\nMonthly\nYearly"
|
||||
},
|
||||
{
|
||||
"fieldname": "stats_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Stats"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-05-01 15:23:29.550243",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Number Card",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Dashboard Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "label, document_type",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "label",
|
||||
"track_changes": 1
|
||||
}
|
||||
144
frappe/desk/doctype/number_card/number_card.py
Normal file
144
frappe/desk/doctype/number_card/number_card.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# -*- 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.utils import cint
|
||||
|
||||
class NumberCard(Document):
|
||||
pass
|
||||
|
||||
|
||||
def get_permission_query_conditions(user=None):
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
||||
if user == 'Administrator':
|
||||
return
|
||||
|
||||
roles = frappe.get_roles(user)
|
||||
if "System Manager" in roles:
|
||||
return None
|
||||
|
||||
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
|
||||
|
||||
return '''
|
||||
`tabNumber Card`.`document_type` in {allowed_doctypes}
|
||||
'''.format(
|
||||
allowed_doctypes=allowed_doctypes,
|
||||
)
|
||||
|
||||
def has_permission(doc, ptype, user):
|
||||
roles = frappe.get_roles(user)
|
||||
if "System Manager" in roles:
|
||||
return True
|
||||
|
||||
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
|
||||
if doc.document_type in allowed_doctypes:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_result(doc, to_date=None):
|
||||
doc = frappe.parse_json(doc)
|
||||
fields = []
|
||||
sql_function_map = {
|
||||
'Count': 'count',
|
||||
'Sum': 'sum',
|
||||
'Average': 'avg',
|
||||
'Minimum': 'min',
|
||||
'Maximum': 'max'
|
||||
}
|
||||
|
||||
function = sql_function_map[doc.function]
|
||||
|
||||
if function == 'count':
|
||||
fields = ['{function}(*) as result'.format(function=function)]
|
||||
else:
|
||||
fields = ['{function}({based_on}) as result'.format(function=function, based_on=doc.aggregate_function_based_on)]
|
||||
|
||||
filters = frappe.parse_json(doc.filters_json)
|
||||
|
||||
if to_date:
|
||||
filters.append([doc.document_type, 'creation', '<', to_date, False])
|
||||
|
||||
res = frappe.db.get_all(doc.document_type, fields=fields, filters=filters)
|
||||
number = res[0]['result'] if res else 0
|
||||
|
||||
return cint(number)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_percentage_difference(doc, result):
|
||||
doc = frappe.parse_json(doc)
|
||||
result = frappe.parse_json(result)
|
||||
|
||||
doc = frappe.get_doc('Number Card', doc.name)
|
||||
|
||||
if not doc.get('show_percentage_stats'):
|
||||
return
|
||||
|
||||
previous_result = calculate_previous_result(doc)
|
||||
difference = (result - previous_result)/100.0
|
||||
|
||||
return difference
|
||||
|
||||
|
||||
def calculate_previous_result(doc):
|
||||
from frappe.utils import add_to_date
|
||||
|
||||
current_date = frappe.utils.now()
|
||||
if doc.stats_time_interval == 'Daily':
|
||||
previous_date = add_to_date(current_date, days=-1)
|
||||
elif doc.stats_time_interval == 'Weekly':
|
||||
previous_date = add_to_date(current_date, weeks=-1)
|
||||
elif doc.stats_time_interval == 'Monthly':
|
||||
previous_date = add_to_date(current_date, months=-1)
|
||||
else:
|
||||
previous_date = add_to_date(current_date, years=-1)
|
||||
|
||||
number = get_result(doc, previous_date)
|
||||
return number
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_number_card(args):
|
||||
args = frappe.parse_json(args)
|
||||
doc = frappe.new_doc('Number Card')
|
||||
|
||||
doc.update(args)
|
||||
doc.insert(ignore_permissions=True)
|
||||
return doc
|
||||
|
||||
def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
|
||||
meta = frappe.get_meta(doctype)
|
||||
searchfields = meta.get_search_fields()
|
||||
search_conditions = []
|
||||
|
||||
if txt:
|
||||
for field in searchfields:
|
||||
search_conditions.append('`tab{doctype}`.`{field}` like %(txt)s'.format(field=field, doctype=doctype, txt=txt))
|
||||
|
||||
search_conditions = ' or '.join(search_conditions)
|
||||
|
||||
search_conditions = 'and (' + search_conditions +')' if search_conditions else ''
|
||||
conditions, values = frappe.db.build_conditions(filters)
|
||||
values['txt'] = '%' + txt + '%'
|
||||
|
||||
return frappe.db.sql(
|
||||
'''select
|
||||
`tabNumber Card`.name, `tabNumber Card`.label, `tabNumber Card`.document_type
|
||||
from
|
||||
`tabNumber Card`
|
||||
where
|
||||
{conditions} and
|
||||
(`tabNumber Card`.owner = '{user}' or
|
||||
`tabNumber Card`.is_public = 1)
|
||||
{search_conditions}
|
||||
'''.format(
|
||||
filters=filters,
|
||||
user=frappe.session.user,
|
||||
search_conditions=search_conditions,
|
||||
conditions=conditions
|
||||
), values)
|
||||
|
|
@ -6,5 +6,5 @@ from __future__ import unicode_literals
|
|||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestCSSClass(unittest.TestCase):
|
||||
class TestNumberCard(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -1,31 +1,27 @@
|
|||
{
|
||||
"creation": "2019-11-19 12:22:42.805741",
|
||||
"actions": [],
|
||||
"creation": "2020-04-19 17:43:50.858343",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"label",
|
||||
"video_id"
|
||||
"card"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "card",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Label"
|
||||
},
|
||||
{
|
||||
"fieldname": "video_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Video"
|
||||
"label": "Card",
|
||||
"options": "Number Card"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-11-19 13:39:57.716248",
|
||||
"links": [],
|
||||
"modified": "2020-04-19 17:45:11.878472",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Onboarding Slide Help Link",
|
||||
"name": "Number Card Link",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
|
|
@ -6,5 +6,5 @@ from __future__ import unicode_literals
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class CSSClass(Document):
|
||||
class NumberCardLink(Document):
|
||||
pass
|
||||
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
|
||||
|
|
@ -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 TestOnboarding(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Onboarding Permission', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2020-04-30 18:27:48.255489",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"role"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "role",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Role",
|
||||
"options": "Role",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-30 18:28:40.423802",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Onboarding Permission",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
# -*- 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 OnboardingSlideHelpLink(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"
|
||||
}
|
||||
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
|
||||
}
|
||||
12
frappe/desk/doctype/onboarding_step/onboarding_step.py
Normal file
12
frappe/desk/doctype/onboarding_step/onboarding_step.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# -*- 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
|
||||
|
||||
class OnboardingStep(Document):
|
||||
def before_export(self, doc):
|
||||
doc.is_complete = 0
|
||||
doc.is_skipped = 0
|
||||
10
frappe/desk/doctype/onboarding_step/test_onboarding_step.py
Normal file
10
frappe/desk/doctype/onboarding_step/test_onboarding_step.py
Normal file
|
|
@ -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 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
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
# -*- 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):
|
||||
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
0
frappe/desk/page/translation_tool/__init__.py
Normal file
0
frappe/desk/page/translation_tool/__init__.py
Normal file
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",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import socket
|
|||
import time
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html
|
||||
from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html, add_days
|
||||
from frappe.utils.user import is_system_user
|
||||
from frappe.utils.jinja import render_template
|
||||
from frappe.email.smtp import SMTPServer
|
||||
|
|
@ -533,28 +533,37 @@ class EmailAccount(Document):
|
|||
parent = None
|
||||
in_reply_to = (email.mail.get("In-Reply-To") or "").strip(" <>")
|
||||
|
||||
if in_reply_to and "@{0}".format(frappe.local.site) in in_reply_to:
|
||||
# reply to a communication sent from the system
|
||||
email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name'])
|
||||
if email_queue:
|
||||
parent_communication, parent_doctype, parent_name = email_queue
|
||||
if parent_communication:
|
||||
communication.in_reply_to = parent_communication
|
||||
if in_reply_to:
|
||||
if "@{0}".format(frappe.local.site) in in_reply_to:
|
||||
# reply to a communication sent from the system
|
||||
email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name'])
|
||||
if email_queue:
|
||||
parent_communication, parent_doctype, parent_name = email_queue
|
||||
if parent_communication:
|
||||
communication.in_reply_to = parent_communication
|
||||
else:
|
||||
reference, domain = in_reply_to.split("@", 1)
|
||||
parent_doctype, parent_name = 'Communication', reference
|
||||
|
||||
if frappe.db.exists(parent_doctype, parent_name):
|
||||
parent = frappe._dict(doctype=parent_doctype, name=parent_name)
|
||||
|
||||
# set in_reply_to of current communication
|
||||
if parent_doctype=='Communication':
|
||||
# communication.in_reply_to = email_queue.communication
|
||||
|
||||
if parent.reference_name:
|
||||
# the true parent is the communication parent
|
||||
parent = frappe.get_doc(parent.reference_doctype,
|
||||
parent.reference_name)
|
||||
else:
|
||||
reference, domain = in_reply_to.split("@", 1)
|
||||
parent_doctype, parent_name = 'Communication', reference
|
||||
|
||||
if frappe.db.exists(parent_doctype, parent_name):
|
||||
parent = frappe._dict(doctype=parent_doctype, name=parent_name)
|
||||
|
||||
# set in_reply_to of current communication
|
||||
if parent_doctype=='Communication':
|
||||
# communication.in_reply_to = email_queue.communication
|
||||
|
||||
if parent.reference_name:
|
||||
# the true parent is the communication parent
|
||||
parent = frappe.get_doc(parent.reference_doctype,
|
||||
parent.reference_name)
|
||||
comm = frappe.db.get_value('Communication',
|
||||
dict(
|
||||
message_id=in_reply_to,
|
||||
creation=['>=', add_days(get_datetime(), -30)]),
|
||||
['reference_doctype', 'reference_name'], as_dict=1)
|
||||
if comm:
|
||||
parent = frappe._dict(doctype=comm.reference_doctype, name=comm.reference_name)
|
||||
|
||||
return parent
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import frappe
|
|||
import sys
|
||||
from six.moves import html_parser as HTMLParser
|
||||
import smtplib, quopri, json
|
||||
from frappe import msgprint, _, safe_decode, safe_encode
|
||||
from frappe import msgprint, _, safe_decode, safe_encode, enqueue
|
||||
from frappe.email.smtp import SMTPServer, get_outgoing_email_account
|
||||
from frappe.email.email_body import get_email, get_formatted_html, add_attachment
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
|
|
@ -347,8 +347,20 @@ def flush(from_test=False):
|
|||
if not smtpserver:
|
||||
smtpserver = SMTPServer()
|
||||
smtpserver_dict[email.sender] = smtpserver
|
||||
|
||||
send_one(email.name, smtpserver, auto_commit, from_test=from_test)
|
||||
|
||||
if from_test:
|
||||
send_one(email.name, smtpserver, auto_commit)
|
||||
else:
|
||||
send_one_args = {
|
||||
'email': email.name,
|
||||
'smtpserver': smtpserver,
|
||||
'auto_commit': auto_commit,
|
||||
}
|
||||
enqueue(
|
||||
method = 'frappe.email.queue.send_one',
|
||||
queue = 'short',
|
||||
**send_one_args
|
||||
)
|
||||
|
||||
# NOTE: removing commit here because we pass auto_commit
|
||||
# finally:
|
||||
|
|
@ -366,7 +378,7 @@ def get_queue():
|
|||
limit 500''', { 'now': now_datetime() }, as_dict=True)
|
||||
|
||||
|
||||
def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=False):
|
||||
def send_one(email, smtpserver=None, auto_commit=True, now=False):
|
||||
'''Send Email Queue with given smtpserver'''
|
||||
|
||||
email = frappe.db.sql('''select
|
||||
|
|
@ -377,8 +389,13 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
|
|||
`tabEmail Queue`
|
||||
where
|
||||
name=%s
|
||||
for update''', email, as_dict=True)[0]
|
||||
|
||||
for update''', email, as_dict=True)
|
||||
|
||||
if len(email):
|
||||
email = email[0]
|
||||
else:
|
||||
return
|
||||
|
||||
recipients_list = frappe.db.sql('''select name, recipient, status from
|
||||
`tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ from frappe.core.doctype.server_script.server_script_utils import run_server_scr
|
|||
from werkzeug.wrappers import Response
|
||||
from six import string_types
|
||||
|
||||
ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet')
|
||||
|
||||
|
||||
def handle():
|
||||
"""handle request"""
|
||||
validate_auth()
|
||||
|
|
@ -148,12 +154,14 @@ def uploadfile():
|
|||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def upload_file():
|
||||
user = None
|
||||
if frappe.session.user == 'Guest':
|
||||
if frappe.get_system_settings('allow_guests_to_upload_files'):
|
||||
ignore_permissions = True
|
||||
else:
|
||||
return
|
||||
else:
|
||||
user = frappe.get_doc("User", frappe.session.user)
|
||||
ignore_permissions = False
|
||||
|
||||
files = frappe.request.files
|
||||
|
|
@ -175,11 +183,11 @@ def upload_file():
|
|||
frappe.local.uploaded_file = content
|
||||
frappe.local.uploaded_filename = filename
|
||||
|
||||
if frappe.session.user == 'Guest':
|
||||
if frappe.session.user == 'Guest' or (user and not user.has_desk_access()):
|
||||
import mimetypes
|
||||
filetype = mimetypes.guess_type(filename)[0]
|
||||
if filetype not in ['image/png', 'image/jpeg', 'application/pdf']:
|
||||
frappe.throw("You can only upload JPG, PNG or PDF files.")
|
||||
if filetype not in ALLOWED_MIMETYPES:
|
||||
frappe.throw(_("You can only upload JPG, PNG, PDF, or Microsoft documents."))
|
||||
|
||||
if method:
|
||||
method = frappe.get_attr(method)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -89,6 +88,7 @@ permission_query_conditions = {
|
|||
"Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions",
|
||||
"Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions",
|
||||
"Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions",
|
||||
"Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions",
|
||||
"Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions",
|
||||
"Note": "frappe.desk.doctype.note.note.get_permission_query_conditions",
|
||||
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions",
|
||||
|
|
@ -105,6 +105,7 @@ has_permission = {
|
|||
"User": "frappe.core.doctype.user.user.has_permission",
|
||||
"Note": "frappe.desk.doctype.note.note.has_permission",
|
||||
"Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.has_permission",
|
||||
"Number Card": "frappe.desk.doctype.number_card.number_card.has_permission",
|
||||
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.has_permission",
|
||||
"Contact": "frappe.contacts.address_and_contact.has_permission",
|
||||
"Address": "frappe.contacts.address_and_contact.has_permission",
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ def make_new_doc(doctype):
|
|||
doc = doc.get_valid_dict(sanitize=False)
|
||||
doc["doctype"] = doctype
|
||||
doc["__islocal"] = 1
|
||||
doc["__unsaved"] = 1
|
||||
|
||||
return doc
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -38,15 +38,17 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
|
|||
("custom", "custom_field"),
|
||||
("custom", "property_setter"),
|
||||
("website", "web_form"),
|
||||
("website", "web_template"),
|
||||
("website", "web_form_field"),
|
||||
("website", "portal_menu_item"),
|
||||
("data_migration", "data_migration_mapping_detail"),
|
||||
("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", "onboarding_permission"),
|
||||
("desk", "onboarding_step"),
|
||||
("desk", "onboarding_step_map"),
|
||||
("desk", "onboarding"),
|
||||
("desk", "desk_card"),
|
||||
("desk", "desk_chart"),
|
||||
("desk", "desk_shortcut"),
|
||||
|
|
@ -78,8 +80,10 @@ 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', 'notification', 'print_style',
|
||||
'data_migration_mapping', 'data_migration_plan', 'onboarding_slide', 'desk_page']
|
||||
'website_theme', 'web_form', 'web_template', 'notification', 'print_style',
|
||||
'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:
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ def start(transaction_type="request", method=None, kwargs=None):
|
|||
|
||||
|
||||
def stop(response=None):
|
||||
if frappe.conf.monitor and hasattr(frappe.local, "monitor"):
|
||||
if hasattr(frappe.local, "monitor"):
|
||||
frappe.local.monitor.dump(response)
|
||||
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -273,3 +273,8 @@ execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
|
|||
frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats
|
||||
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders()
|
||||
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")
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
|
||||
|
||||
if not frappe.db.table_exists('Dashboard Chart'):
|
||||
return
|
||||
|
||||
users_with_permission = frappe.get_all(
|
||||
"Has Role",
|
||||
fields=["parent"],
|
||||
filters={"role": ['in', ['System Manager', 'Dashboard Manager']], "parenttype": "User"},
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc('website', 'doctype', 'web_page_view', force=True)
|
||||
frappe.db.sql("""UPDATE `tabWeb Page View` set path="/" where path=''""")
|
||||
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,7 @@ def execute():
|
|||
frappe.reload_doctype('Website Theme')
|
||||
for theme in frappe.get_all('Website Theme'):
|
||||
doc = frappe.get_doc('Website Theme', theme.name)
|
||||
if not doc.custom_scss and doc.theme_scss:
|
||||
if not doc.get('custom_scss') and doc.theme_scss:
|
||||
# move old theme to new theme
|
||||
doc.custom_scss = doc.theme_scss
|
||||
doc.save()
|
||||
doc.save()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue