Merge branch 'develop' into setup-search

This commit is contained in:
Suraj Shetty 2020-05-05 14:40:30 +05:30 committed by GitHub
commit 5c8dde1deb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
332 changed files with 260031 additions and 256909 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);
});
});
});

View file

@ -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'''

View file

@ -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",

View file

@ -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

View file

@ -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"
}

View file

@ -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();
}
});
},

View file

@ -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,

View file

@ -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"]],

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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>`;
})

View file

@ -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)

View file

@ -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",

View file

@ -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

View file

@ -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() {
//
}
});

View file

@ -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
}

View file

@ -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)

View file

@ -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() {

View file

@ -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());

View file

@ -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):

View file

@ -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."""

View file

@ -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()

View file

@ -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)

View file

@ -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
}
};
});
}
});

View file

@ -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",

View file

@ -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

View file

@ -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', () => {

View file

@ -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,

View file

@ -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):

View file

@ -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",

View 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);
}
}
});

View 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
}

View 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)

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
class TestCSSClass(unittest.TestCase):
class TestNumberCard(unittest.TestCase):
pass

View file

@ -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,

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class CSSClass(Document):
class NumberCardLink(Document):
pass

View 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();
},
});

View 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
}

View 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

View file

@ -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

View file

@ -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) {
// }
});

View file

@ -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
}

View file

@ -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

View 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 TestOnboardingPermission(unittest.TestCase):
pass

View file

@ -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');
}
}
});

View file

@ -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
}

View file

@ -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]

View file

@ -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"
}

View 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);
});
}
}

View 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
}

View 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

View 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

View file

@ -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
}

View file

@ -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

View file

@ -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",

View file

@ -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')

View 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;
}

View 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>

View 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}`;
}
}

View 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"
}

View file

@ -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();
});
}

View file

@ -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",

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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",

View file

@ -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

View file

@ -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')

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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",

View file

@ -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",

View file

@ -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)

View file

@ -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"))

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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:

View file

@ -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:

View file

@ -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

View 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")

View 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")

View file

@ -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)

View file

@ -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=''""")

View 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)

View file

@ -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()

View file

@ -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