Merge branch 'develop' into snyk-fix-1b018faa364b5dc0e26502cba6099d54

This commit is contained in:
Suraj Shetty 2020-05-08 14:53:05 +05:30 committed by GitHub
commit abc2ec1496
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
228 changed files with 255479 additions and 256818 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

@ -1,5 +1,5 @@
language: python
dist: trusty
dist: bionic
addons:
hosts:
@ -9,6 +9,10 @@ addons:
postgresql: 9.5
chrome: stable
services:
- xvfb
- mysql
git:
depth: 1
@ -23,29 +27,24 @@ cache:
matrix:
include:
- name: "Python 3.6 MariaDB"
python: 3.6
- name: "Python 3.7 MariaDB"
python: 3.7
env: DB=mariadb TYPE=server
script: bench --site test_site run-tests --coverage
- name: "Python 3.6 PostgreSQL"
python: 3.6
- name: "Python 3.7 PostgreSQL"
python: 3.7
env: DB=postgres TYPE=server
script: bench --site test_site run-tests --coverage
- name: "Cypress"
python: 3.6
python: 3.7
env: DB=mariadb TYPE=ui
before_script:
- bench --site test_site execute frappe.utils.install.complete_setup_wizard
- bench --site test_site_producer execute frappe.utils.install.complete_setup_wizard
script: bench --site test_site run-ui-tests frappe --headless
- name: "Python 2.7 MariaDB"
python: 2.7
env: DB=mariadb TYPE=server
script: bench --site test_site run-tests --coverage
before_install:
# install wkhtmltopdf
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz

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

@ -23,7 +23,7 @@ if PY2:
reload(sys)
sys.setdefaultencoding("utf-8")
__version__ = '12.0.0-dev'
__version__ = '13.0.0-dev'
__title__ = "Frappe Framework"
local = Local()
@ -48,8 +48,12 @@ class _dict(dict):
def copy(self):
return _dict(dict(self).copy())
def _(msg, lang=None):
"""Returns translated string in current lang, if exists."""
def _(msg, lang=None, context=None):
"""Returns translated string in current lang, if exists.
Usage:
_('Change')
_('Change', context='Coins')
"""
from frappe.translate import get_full_dict
from frappe.utils import strip_html_tags, is_html
@ -59,7 +63,7 @@ def _(msg, lang=None):
if not lang:
lang = local.lang
non_translated_msg = msg
non_translated_string = msg
if is_html(msg):
msg = strip_html_tags(msg)
@ -67,8 +71,16 @@ def _(msg, lang=None):
# msg should always be unicode
msg = as_unicode(msg).strip()
translated_string = ''
if context:
string_key = '{msg}:{context}'.format(msg=msg, context=context)
translated_string = get_full_dict(lang).get(string_key)
if not translated_string:
translated_string = get_full_dict(lang).get(msg)
# return lang_full_dict according to lang passed parameter
return get_full_dict(lang).get(msg) or non_translated_msg
return translated_string or non_translated_string
def as_unicode(text, encoding='utf-8'):
'''Convert to unicode if required'''

View file

@ -81,7 +81,7 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
installing = touch_file(get_site_path('locks', 'installing.lock'))
atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password)
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
@ -101,7 +101,7 @@ def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password):
if installing and os.path.exists(installing):
if mariadb_root_password:
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True)
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True, no_backup=True)
shutil.rmtree(site)
frappe.destroy()

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

@ -26,6 +26,7 @@ class Comment(Document):
def validate(self):
if not self.comment_email:
self.comment_email = frappe.session.user
self.content = frappe.utils.sanitize_html(self.content)
def on_update(self):
update_comment_in_doc(self)

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

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

@ -102,7 +102,7 @@ class User(Document):
'frappe.core.doctype.user.user.create_contact',
user=self,
ignore_mandatory=True,
now=frappe.flags.in_test
now=frappe.flags.in_test or frappe.flags.in_install
)
if self.name not in ('Administrator', 'Guest') and not self.user_image:
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)

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

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

@ -224,15 +224,14 @@
},
{
"default": "0",
"description": "This chart will be public to all Users if this is set",
"description": "This chart will be available to all Users if this is set",
"fieldname": "is_public",
"fieldtype": "Check",
"label": "Is Public",
"permlevel": 1
"label": "Is Public"
}
],
"links": [],
"modified": "2020-04-23 13:01:07.178866",
"modified": "2020-05-01 15:22:59.119341",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",

View file

@ -7,7 +7,7 @@ import frappe
from frappe import _
import datetime
import json
from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan
from frappe.utils.dashboard import cache_source, get_from_date_from_timespan
from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime
from frappe.model.naming import append_number_if_name_exists
from frappe.boot import get_allowed_reports
@ -27,7 +27,7 @@ def get_permission_query_conditions(user):
return None
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()])
allowed_reports = tuple([key if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()])
return '''
`tabDashboard Chart`.`document_type` in {allowed_doctypes}
@ -76,7 +76,7 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
if to_date and len(to_date):
to_date = get_datetime(to_date)
else:
to_date = chart.to_date
to_date = get_datetime(chart.to_date)
timegrain = time_interval or chart.time_interval
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or []
@ -110,7 +110,8 @@ def create_dashboard_chart(args):
@frappe.whitelist()
def create_report_chart(args):
create_dashboard_chart()
create_dashboard_chart(args)
args = frappe.parse_json(args)
if args.dashboard:
add_chart_to_dashboard(json.dumps(args))

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

@ -72,11 +72,10 @@
},
{
"default": "0",
"description": "This card will be public to all Users if this is set",
"description": "This card will be available to all Users if this is set",
"fieldname": "is_public",
"fieldtype": "Check",
"label": "Is Public",
"permlevel": 1
"label": "Is Public"
},
{
"default": "1",
@ -100,7 +99,7 @@
}
],
"links": [],
"modified": "2020-04-25 17:31:34.204607",
"modified": "2020-05-01 15:23:29.550243",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",

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

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

View file

@ -1,7 +1,7 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Web View', {
frappe.ui.form.on('Onboarding Permission', {
// refresh: function(frm) {
// }

View file

@ -1,31 +1,28 @@
{
"creation": "2019-11-19 12:22:42.805741",
"actions": [],
"creation": "2020-04-30 18:27:48.255489",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"label",
"video_id"
"role"
],
"fields": [
{
"fieldname": "label",
"fieldtype": "Data",
"fieldname": "role",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Label"
},
{
"fieldname": "video_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Video"
"label": "Role",
"options": "Role",
"reqd": 1
}
],
"istable": 1,
"modified": "2019-11-19 13:39:57.716248",
"links": [],
"modified": "2020-04-30 18:28:40.423802",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Slide Help Link",
"name": "Onboarding Permission",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,

View file

@ -6,5 +6,6 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class CSSClass(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

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class OnboardingSlideHelpLink(Document):
pass

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

@ -1,10 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class OnboardingSlideField(Document):
pass
class OnboardingStep(Document):
def before_export(self, doc):
doc.is_complete = 0
doc.is_skipped = 0

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

@ -6,5 +6,6 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class WebViewComponent(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",
@ -107,7 +112,7 @@ class Leaderboard {
this.timespans.map(d => {
return {"label": __(d), value: d };
})
);
);
this.create_from_date_field();
this.type_select = this.page.add_select(__("Field"),
@ -188,30 +193,12 @@ class Leaderboard {
this.$search_box =
$(`<div class="leaderboard-search form-group col-md-3">
<input type="text" placeholder="Search" class="form-control leaderboard-search-input input-sm">
<input type="text" placeholder="Search" data-element="search" class="form-control leaderboard-search-input input-sm">
</div>`);
$(this.parent).find(".page-form").append(this.$search_box);
}
setup_search(list_items) {
let $search_input = this.$search_box.find(".leaderboard-search-input");
this.$search_box.on("keyup", ()=> {
let text_filter = $search_input.val().toLowerCase();
text_filter = text_filter.replace(/^\s+|\s+$/g, '');
for (var i = 0; i < list_items.length; i++) {
let text = list_items.eq(i).find(".list-id").text().trim().toLowerCase();
if (text.includes(text_filter)) {
list_items.eq(i).css("display", "");
} else {
list_items.eq(i).css("display", "none");
}
}
});
}
show_leaderboard(doctype) {
if (this.doctypes.length) {
if (this.doctypes.includes(doctype)) {
@ -273,7 +260,7 @@ class Leaderboard {
if (res && res.message.length) {
me.message = null;
me.$container.find(".leaderboard-list").html(me.render_list_view(res.message));
me.setup_search($(me.parent).find('.list-item-container'));
frappe.utils.setup_search($(me.parent), ".list-item-container", ".list-id");
} else {
me.$graph_area.hide();
me.message = __("No items found.");

View file

@ -6,12 +6,14 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.desk.doctype.global_search_settings.global_search_settings import update_global_search_doctypes
from frappe.utils.dashboard import sync_dashboards
def install():
update_genders()
update_salutations()
update_global_search_doctypes()
setup_email_linking()
sync_dashboards()
@frappe.whitelist()
def update_genders():
@ -35,4 +37,3 @@ def setup_email_linking():
"email_id": "email_linking@example.com",
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)

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

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

@ -75,22 +75,6 @@ This is the text version of this email
else:
self.assertTrue(True)
def test_rfc_5322_header_is_wrapped_at_998_chars(self):
# unfortunately the db can only hold 140 chars so this can't be tested properly. test at max chars anyway.
email = get_email_queue(
recipients=['test@example.com'],
sender='me@example.com',
subject='Test Subject',
content='<h1>Whatever</h1>',
text_content='whatever',
message_id="a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
".really.long.message.id.that.should.not.wrap.unti")
result = safe_decode(prepare_message(email=email, recipient='test@test.com',
recipients_list=[]))
self.assertTrue(
"a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
".really.long.message.id.that.should.not.wrap.unti" in result)
def test_image(self):
img_signature = '''
Content-Type: image/png

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

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

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

@ -10,6 +10,7 @@ import frappe.translate
import frappe.modules.patch_handler
import frappe.model.sync
from frappe.utils.fixtures import sync_fixtures
from frappe.utils.dashboard import sync_dashboards
from frappe.cache_manager import clear_global_cache
from frappe.desk.notifications import clear_notifications
from frappe.website import render
@ -23,6 +24,7 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False):
- run before migrate hooks
- run patches
- sync doctypes (schema)
- sync dashboards
- sync fixtures
- sync desktop icons
- sync web pages (from /www)
@ -53,6 +55,7 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False):
frappe.translate.clear_cache()
sync_jobs()
sync_fixtures()
sync_dashboards()
sync_customizations()
sync_languages()

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

@ -45,9 +45,13 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
("data_migration", "data_migration_mapping"),
("data_migration", "data_migration_plan_mapping"),
("data_migration", "data_migration_plan"),
("desk", "onboarding_slide_field"),
("desk", "onboarding_slide_help_link"),
("desk", "onboarding_slide"),
("desk", "number_card"),
("desk", "dashboard_chart"),
("desk", "dashboard"),
("desk", "onboarding_permission"),
("desk", "onboarding_step"),
("desk", "onboarding_step_map"),
("desk", "onboarding"),
("desk", "desk_card"),
("desk", "desk_chart"),
("desk", "desk_shortcut"),
@ -80,7 +84,9 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F
# load in sequence - warning for devs
document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
'website_theme', 'web_form', 'web_template', 'notification', 'print_style',
'data_migration_mapping', 'data_migration_plan', 'onboarding_slide', 'desk_page']
'data_migration_mapping', 'data_migration_plan', 'desk_page',
'onboarding_step', 'onboarding']
for doctype in document_types:
doctype_path = os.path.join(start_path, doctype)
if os.path.exists(doctype_path):

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

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

@ -275,3 +275,6 @@ execute:from frappe.desk.page.setup_wizard.install_fixtures import update_gender
frappe.patches.v13_0.website_theme_custom_scss
frappe.patches.v13_0.set_existing_dashboard_charts_as_public
frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
frappe.patches.v13_0.migrate_translation_column_data
frappe.patches.v13_0.set_read_times
frappe.patches.v13_0.remove_web_view

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

@ -11,19 +11,11 @@ def execute():
fields=["parent"],
filters={"role": ['in', ['System Manager', 'Dashboard Manager']], "parenttype": "User"},
distinct=True,
as_list=True
)
users = tuple(
[item if type(item) == str else item.encode('utf8') for sublist in users_with_permission for item in sublist]
)
users = [item.parent for item in users_with_permission]
charts = frappe.db.get_all('Dashboard Chart', filters={'owner': ['in', users]})
for chart in charts:
frappe.db.set_value('Dashboard Chart', chart.name, 'is_public', 1)
frappe.db.sql("""
UPDATE
`tabDashboard Chart`
SET
`tabDashboard Chart`.`is_public`=1
WHERE
`tabDashboard Chart`.owner in {users}
""".format(users=users)
)

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

View file

@ -1,37 +0,0 @@
{
"add_more_button": 0,
"app": "ERPNext",
"creation": "2019-11-22 13:25:42.892593",
"docstatus": 0,
"doctype": "Onboarding Slide",
"domains": [],
"help_links": [
{
"label": "Need Help?",
"video_id": "cKZHcx1znMc"
}
],
"idx": 0,
"image_src": "",
"is_completed": 1,
"max_count": 0,
"modified": "2019-12-09 15:12:45.588567",
"modified_by": "Administrator",
"name": "Company Letter Head",
"owner": "Administrator",
"ref_doctype": "Letter Head",
"slide_desc": "<p>The letter head will appear across all print formats and PDFs</p>\n<p class=\"text-muted\">Keep it web friendly as 1024px by 128px</p>",
"slide_fields": [
{
"align": "center",
"fieldname": "letterhead",
"fieldtype": "Attach Image",
"label": "Attach Letterhead",
"options": "image",
"reqd": 0
}
],
"slide_order": 20,
"slide_title": "Company Letter Head",
"slide_type": "Create"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -2,7 +2,7 @@
// MIT License. See license.txt
// library to mange assets (js, css, models, html) etc in the app.
// will try and get from localStorge if latest are available
// will try and get from localStorage if latest are available
// depends on frappe.versions to manage versioning
frappe.require = function(items, callback) {

View file

@ -96,10 +96,6 @@ frappe.Application = Class.extend({
this.show_notes();
if (frappe.boot.is_first_startup) {
this.setup_onboarding_wizard();
}
if (frappe.ui.startup_setup_dialog && !frappe.boot.setup_complete) {
frappe.ui.startup_setup_dialog.pre_show();
frappe.ui.startup_setup_dialog.show();
@ -512,20 +508,6 @@ frappe.Application = Class.extend({
});
},
setup_onboarding_wizard: () => {
frappe.call('frappe.desk.doctype.onboarding_slide.onboarding_slide.get_onboarding_slides').then(res => {
if (res.message) {
let slides = res.message;
if (slides.length) {
this.progress_dialog = new frappe.setup.OnboardingDialog({
slides: slides
});
this.progress_dialog.show();
}
}
});
},
setup_analytics: function() {
if(window.mixpanel) {
window.mixpanel.identify(frappe.session.user);

View file

@ -114,7 +114,7 @@ frappe.ui.form.Control = Class.extend({
if (!this.doc.__islocal) {
new frappe.views.TranslationManager({
'df': this.df,
'source_name': value,
'source_text': value,
'target_language': this.doc.language,
'doc': this.doc
});

View file

@ -16,7 +16,7 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({
$(this.input_area).find('i').hover((ev) => {
const el = $(ev.currentTarget);
let star_value = el.data('rating');
el.parent().children('i.fa').each( function(e){
el.parent().children('i.fa').each( function(e) {
if (e < star_value) {
$(this).addClass('star-hover');
} else {

View file

@ -54,6 +54,22 @@ Quill.register(FontStyle, true);
Quill.register(AlignStyle, true);
Quill.register(DirectionStyle, true);
// replace font tag with span
const Inline = Quill.import('blots/inline');
class CustomColor extends Inline {
constructor(domNode, value) {
super(domNode, value);
this.domNode.style.color = this.domNode.color;
domNode.outerHTML = this.domNode.outerHTML.replace(/<font/g, '<span').replace(/<\/font>/g, '</span>');
}
}
CustomColor.blotName = "customColor";
CustomColor.tagName = "font";
Quill.register(CustomColor, true);
frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
make_wrapper() {
this._super();

View file

@ -439,6 +439,7 @@ frappe.ui.form.Form = class FrappeForm {
return this.script_manager.trigger("onload_post_render");
}
},
() => this.run_after_load_hook(),
() => this.dashboard.after_refresh()
]);
// focus on first input
@ -462,6 +463,15 @@ frappe.ui.form.Form = class FrappeForm {
this.scroll_to_element();
}
run_after_load_hook() {
if (frappe.route_options.after_load) {
let route_callback = frappe.route_options.after_load;
delete frappe.route_options.after_load;
route_callback(this);
}
}
refresh_fields() {
this.layout.refresh(this.doc);
this.layout.primary_button = this.$wrapper.find(".btn-primary");
@ -569,6 +579,13 @@ frappe.ui.form.Form = class FrappeForm {
}
me.script_manager.trigger("after_save");
if (frappe.route_options.after_save) {
let route_callback = frappe.route_options.after_save;
delete frappe.route_options.after_save;
route_callback(me);
}
// submit comment if entered
if (me.timeline) {
me.timeline.comment_area.submit();
@ -1530,7 +1547,7 @@ frappe.ui.form.Form = class FrappeForm {
}
// scroll to input
frappe.utils.scroll_to($el);
frappe.utils.scroll_to($el, true, 15);
// highlight input
$el.addClass('has-error');

View file

@ -6,7 +6,7 @@ frappe.quick_edit = function(doctype, name) {
});
};
frappe.ui.form.make_quick_entry = (doctype, after_insert, init_callback, doc) => {
frappe.ui.form.make_quick_entry = (doctype, after_insert, init_callback, doc, force) => {
var trimmed_doctype = doctype.replace(/ /g, '');
var controller_name = "QuickEntryForm";
@ -14,31 +14,31 @@ frappe.ui.form.make_quick_entry = (doctype, after_insert, init_callback, doc) =>
controller_name = trimmed_doctype + "QuickEntryForm";
}
frappe.quick_entry = new frappe.ui.form[controller_name](doctype, after_insert, init_callback, doc);
frappe.quick_entry = new frappe.ui.form[controller_name](doctype, after_insert, init_callback, doc, force);
return frappe.quick_entry.setup();
};
frappe.ui.form.QuickEntryForm = Class.extend({
init: function(doctype, after_insert, init_callback, doc) {
init: function(doctype, after_insert, init_callback, doc, force) {
this.doctype = doctype;
this.after_insert = after_insert;
this.init_callback = init_callback;
this.doc = doc;
this.force = force ? force : false;
},
setup: function() {
let me = this;
return new Promise(resolve => {
frappe.model.with_doctype(this.doctype, function() {
me.check_quick_entry_doc();
me.set_meta_and_mandatory_fields();
if(me.is_quick_entry()) {
me.render_dialog();
resolve(me);
frappe.model.with_doctype(this.doctype, () => {
this.check_quick_entry_doc();
this.set_meta_and_mandatory_fields();
if (this.is_quick_entry() || this.force) {
this.render_dialog();
resolve(this);
} else {
frappe.quick_entry = null;
frappe.set_route('Form', me.doctype, me.doc.name)
.then(() => resolve(me));
frappe.set_route('Form', this.doctype, this.doc.name)
.then(() => resolve(this));
}
});
});
@ -49,8 +49,8 @@ frappe.ui.form.QuickEntryForm = Class.extend({
let fields = this.meta.fields;
// prepare a list of mandatory, bold and allow in quick entry fields
this.mandatory = $.map(fields, function(d) {
return ((d.reqd || d.bold || d.allow_in_quick_entry) && !d.read_only) ? $.extend({}, d) : null;
this.mandatory = fields.filter(df => {
return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only);
});
},
@ -187,7 +187,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
},
error: function() {
if (!me.skip_redirect_on_error) {
me.open_doc();
me.open_doc(true);
}
},
always: function() {
@ -245,9 +245,15 @@ frappe.ui.form.QuickEntryForm = Class.extend({
return this.dialog.doc;
},
open_doc: function(){
open_doc: function(set_hooks) {
this.dialog.hide();
this.update_doc();
if (set_hooks && this.after_insert) {
frappe.route_options = frappe.route_options || {};
frappe.route_options.after_save = (frm) => {
this.after_insert(frm);
};
}
frappe.set_route('Form', this.doctype, this.doc.name);
},
@ -258,7 +264,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
$link.find('.edit-full').on('click', function() {
// edit in form
me.open_doc();
me.open_doc(true);
});
},

View file

@ -21,6 +21,12 @@ frappe.ui.form.Review = class Review {
});
}
make_review_container() {
this.parent.append(`
<ul class="list-unstyled sidebar-menu">
<li class="h6 reviews-label">${__('Reviews')}</li>
<li class="review-list"></li>
</ul>
`);
this.review_list_wrapper = this.parent.find('.review-list');
}
add_review_button() {

View file

@ -69,10 +69,7 @@
<div class="clearfix"></div>
</li>
</ul>
<ul class="list-unstyled sidebar-menu form-reviews">
<li class="h6 attachments-label">{%= __("Reviews") %}</li>
<li class="review-list"></li>
</ul>
<span class="form-reviews"></span>
<ul class="list-unstyled sidebar-menu">
<li class="h6 shared-with-label">{%= __("Shared With") %}</li>
<li class="form-shared"></li>

View file

@ -203,6 +203,7 @@ frappe.views.BaseList = class BaseList {
show_sidebar = !show_sidebar;
localStorage.show_sidebar = show_sidebar;
this.show_or_hide_sidebar();
$(document.body).trigger('toggleListSidebar');
}
show_or_hide_sidebar() {

View file

@ -71,7 +71,7 @@
</a>
<ul class="dropdown-menu list-stats-dropdown" role="menu">
<div class="dropdown-search">
<input type="text" placeholder="Search" class="form-control dropdown-search-input input-xs">
<input type="text" placeholder="Search" data-element="search" class="form-control input-xs">
</div>
</ul>
</div>

View file

@ -209,11 +209,13 @@ frappe.views.ListSidebar = class ListSidebar {
accounts.forEach((account) => {
let email_account = (account.email_id == "All Accounts") ? "All Accounts" : account.email_account;
let route = ["List", "Communication", "Inbox", email_account].join('/');
let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes(account.email_id) ? __(account.email_id) : account.email_id;
if (!divider) {
this.get_divider().appendTo($dropdown);
divider = true;
}
$(`<li><a href="#${route}">${account.email_id}</a></li>`).appendTo($dropdown);
$(`<li><a href="#${route}">${display_name}</a></li>`).appendTo($dropdown);
if (account.email_id === "Sent Mail")
divider = false;
});
@ -240,40 +242,6 @@ frappe.views.ListSidebar = class ListSidebar {
});
}
setup_dropdown_search(dropdown, text_class) {
let $dropdown_search = dropdown.find('.dropdown-search').show();
let $search_input = $dropdown_search.find('.dropdown-search-input');
$search_input.focus();
$dropdown_search.on('click',(e)=>{
e.stopPropagation();
});
let $elements = dropdown.find('li');
$dropdown_search.on('keyup',()=> {
let text_filter = $search_input.val().toLowerCase();
// Replace trailing and leading spaces
text_filter = text_filter.replace(/^\s+|\s+$/g, '');
for (var i = 0; i < $elements.length; i++) {
let text_element = $elements.eq(i).find(text_class);
let text = text_element.text().toLowerCase();
// Search data-name since label for current user is 'Me'
let name = '';
if (text_element.data('name')) {
name = text_element.data('name').toLowerCase();
}
if (text.includes(text_filter) || name.includes(text_filter)) {
$elements.eq(i).css('display','');
} else {
$elements.eq(i).css('display','none');
}
}
});
dropdown.parent().on('hide.bs.dropdown',()=> {
$dropdown_search.val('');
});
}
get_cat_tags() {
return this.cat_tags;
}
@ -292,7 +260,7 @@ frappe.views.ListSidebar = class ListSidebar {
callback: function(r) {
me.render_stat("_user_tags", (r.message.stats || {})["_user_tags"]);
let stats_dropdown = me.sidebar.find('.list-stats-dropdown');
me.setup_dropdown_search(stats_dropdown,'.stat-label');
frappe.utils.setup_search(stats_dropdown, '.stat-link', '.stat-label');
}
});
}

View file

@ -22,6 +22,7 @@ frappe.views.ListGroupBy = class ListGroupBy {
title: __("Select Filters"),
fields: this.get_group_by_dropdown_fields()
});
d.set_primary_action("Save", ({ group_by_fields }) => {
frappe.model.user_settings.save(this.doctype, 'group_by_fields', group_by_fields || null);
this.group_by_fields = group_by_fields ? ['assigned_to', 'owner', ...group_by_fields] : ['assigned_to', 'owner'];
@ -29,7 +30,12 @@ frappe.views.ListGroupBy = class ListGroupBy {
d.hide();
});
this.page.sidebar.find(".add-list-group-by a ").on("click", () => {
d.$body.prepend(`<div class="filters-search">
<input type="text" placeholder="${__('Search')}" data-element="search" class="form-control input-xs">
</div>`);
this.page.sidebar.find(".add-list-group-by a").on("click", () => {
frappe.utils.setup_search(d.$body, '.unit-checkbox', '.label-area');
d.show();
});
}
@ -95,7 +101,7 @@ frappe.views.ListGroupBy = class ListGroupBy {
this.get_group_by_count(fieldname).then(field_count_list => {
if (field_count_list.length) {
this.render_dropdown_items(field_count_list, fieldtype, dropdown);
this.sidebar.setup_dropdown_search(dropdown, '.group-by-value');
frappe.utils.setup_search(dropdown, '.group-by-item', '.group-by-value', 'data-name');
} else {
dropdown.find('.group-by-loading').html(`${__("No filters found")}`);
}
@ -165,7 +171,7 @@ frappe.views.ListGroupBy = class ListGroupBy {
};
let standard_html = `
<div class="dropdown-search">
<input type="text" placeholder="${__('Search')}" class="form-control dropdown-search-input input-xs">
<input type="text" placeholder="${__('Search')}" data-element="search" class="dropdown-search-input form-control input-xs">
</div>
`;

View file

@ -3,26 +3,41 @@
// for translation
frappe._messages = {};
frappe._ = function(txt, replace) {
if(!txt)
return txt;
if(typeof(txt) != "string")
return txt;
var ret = frappe._messages[txt.replace(/\n/g, "")] || txt;
if(replace && typeof(replace) === "object") {
ret = $.format(ret, replace);
frappe._ = function(txt, replace, context = null) {
if ($.isEmptyObject(frappe._messages) && frappe.boot) {
$.extend(frappe._messages, frappe.boot.__messages);
}
return ret;
if (!txt) return txt;
if (typeof txt != "string") return txt;
let translated_text = '';
let key = txt.replace(/\n/g, "");
if (context) {
translated_text = frappe._messages[`${key}:${context}`];
}
if (!translated_text) {
translated_text = frappe._messages[key] || txt;
}
if (replace && typeof replace === "object") {
translated_text = $.format(translated_text, replace);
}
return translated_text;
};
window.__ = frappe._
window.__ = frappe._;
frappe.get_languages = function() {
if(!frappe.languages) {
frappe.languages = []
$.each(frappe.boot.lang_dict, function(lang, value){
frappe.languages.push({'label': lang, 'value': value})
if (!frappe.languages) {
frappe.languages = [];
$.each(frappe.boot.lang_dict, function(lang, value) {
frappe.languages.push({ label: lang, value: value });
});
frappe.languages = frappe.languages.sort(function(a, b) {
return a.value < b.value ? -1 : 1;
});
frappe.languages = frappe.languages.sort(function(a, b) { return (a.value < b.value) ? -1 : 1 });
}
return frappe.languages;
};

View file

@ -146,6 +146,12 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.get_close_btn().on('click', click);
}
set_secondary_action_label(label) {
this.get_close_btn()
.removeClass("hide")
.html(label);
}
disable_primary_action() {
this.get_primary_btn().addClass('disabled');
}

View file

@ -169,6 +169,11 @@ frappe.msgprint = function(msg, title, is_minimizable) {
}
}
if (data.secondary_action) {
frappe.msg_dialog.set_secondary_action(data.secondary_action.action);
frappe.msg_dialog.set_secondary_action_label(__(data.secondary_action.label || "Close"));
}
if(data.message==null) {
data.message = '';
}

View file

@ -1,179 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.provide("frappe.setup");
frappe.provide("frappe.ui");
frappe.setup.OnboardingSlide = class OnboardingSlide extends frappe.ui.Slide {
constructor(slide = null) {
super(slide);
}
make() {
super.make();
this.$next_btn = this.slides_footer.find('.next-btn');
this.$complete_btn = this.slides_footer.find('.complete-btn');
this.$action_button = this.slides_footer.find('.next-btn');
if (this.help_links) {
this.$help_links = $(`<div class="text-center">
<div class="help-links"></div>
</div>`).appendTo(this.$body);
this.setup_help_links();
}
this.$skip_btn = this.slides_footer.find('.skip-btn').on('click', () => {
$('.onboarding-dialog').modal('toggle');
});
}
setup_form() {
super.setup_form();
const fields = this.get_atomic_fields();
// remove link indicator
fields.map((field) => {
if (field.fieldtype == 'Link') {
$('.link-btn').remove();
}
});
if (fields.length == 1) {
this.$form_wrapper.addClass("text-center");
} else {
this.$form_wrapper.removeClass("text-center");
}
}
before_show() {
if (this.id === 0) {
this.$next_btn.text(__('Let\'s Go'));
this.$skip_btn.removeClass('hide');
} else {
this.$next_btn.text(__('Next'));
this.$skip_btn.addClass('hide');
}
//last slide
if (this.is_last_slide()) {
this.$complete_btn.removeClass('hide').addClass('action primary');
this.$next_btn.removeClass('action primary');
this.$action_button = this.$complete_btn;
}
this.setup_action_button();
}
primary_action() {
if (this.set_values()) {
this.$action_button.addClass('disabled');
const primary_method = 'frappe.desk.doctype.onboarding_slide.onboarding_slide.create_onboarding_docs';
if (this.add_more) {
this.values.max_count = this.max_count;
}
frappe.call(primary_method, {
values: this.values,
doctype: this.ref_doctype,
app: this.app,
slide_type: this.slide_type
}).then(() => {
if (this.is_last_slide()) {
this.reset_is_first_startup();
$('.onboarding-dialog').modal('toggle');
frappe.msgprint({
message: __('You are all set up!'),
indicator: 'green',
title: __('Success')
});
}
});
}
}
unbind_primary_action() {
// unbind only action method as next button is same as create button in this setup wizard
this.$action_button.off('click.primary_action');
}
setup_help_links() {
this.help_links.map(link => {
let $link = $(
`<a target="_blank" class="small text-muted">${link.label || __("Need Help?")}</a>`
);
if (link.video_id) {
$link.on('click', () => {
frappe.help.show_video(link.video_id, link.label);
});
}
this.$help_links.append($link);
});
}
setup_action_button() {
if (this.slide_type === 'Create' || this.slide_type == 'Settings' || this.is_last_slide()) {
this.$action_button.addClass('primary');
} else {
this.$action_button.removeClass('primary');
}
this.$action_button.on('click', () => {
if (this.slide_type != 'Continue') {
this.mark_as_completed();
}
});
}
mark_as_completed() {
frappe.call({
method: 'frappe.desk.doctype.onboarding_slide.onboarding_slide.mark_slide_as_completed',
args: {slide_title: this.title},
callback: () => {},
freeze: true
});
}
reset_is_first_startup() {
frappe.call({
method: "frappe.desk.page.setup_wizard.setup_wizard.reset_is_first_startup",
args: {},
callback: () => {}
});
}
};
frappe.setup.OnboardingDialog = class OnboardingDialog {
constructor({
slides = []
}) {
this.slides = slides;
this.setup();
}
setup() {
this.dialog = new frappe.ui.Dialog({
static: true,
minimizable: false,
});
this.$wrapper = $(this.dialog.$wrapper).addClass('onboarding-dialog');
this.slide_container = new frappe.ui.Slides({
parent: this.dialog.body,
slides: this.slides,
slide_class: frappe.setup.OnboardingSlide,
unidirectional: 1,
before_load: ($footer) => {
$footer.find('.prev-btn').addClass('hide');
$footer.find('.next-btn').removeClass('btn-default').addClass('btn-primary action');
$footer.find('.prev-div').prepend(
$(`<a class="skip-btn text-muted btn btn-link btn-sm hide">
${__("Do It Later")}</a>`));
$footer.find('.next-div').prepend(
$(`<a class="complete-btn btn btn-primary btn-sm hide">
${__("Complete")}</a>`));
}
});
this.$wrapper.find('.modal-header').remove();
}
show() {
this.dialog.show();
}
};

View file

@ -16,6 +16,11 @@ frappe.help.show = function(doctype) {
}
frappe.help.show_video = function(youtube_id, title) {
if (frappe.utils.is_url(youtube_id)) {
const expression = '(?:youtube.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu.be/)([^\"&?/s]{11})';
youtube_id = youtube_id.match(expression)[1];
}
if($("body").width() > 768) {
var size = [670, 377];
} else {

View file

@ -695,7 +695,32 @@ Object.assign(frappe.utils, {
return null;
}
},
setup_search($wrapper, el_class, text_class, data_attr) {
const $search_input = $wrapper.find('[data-element="search"]').show();
$search_input.focus().val('');
const $elements = $wrapper.find(el_class).show();
$search_input.off('keyup').on('keyup', () => {
let text_filter = $search_input.val().toLowerCase();
// Replace trailing and leading spaces
text_filter = text_filter.replace(/^\s+|\s+$/g, '');
for (let i = 0; i < $elements.length; i++) {
const text_element = $elements.eq(i).find(text_class);
const text = text_element.text().toLowerCase();
let name = '';
if (data_attr && text_element.attr(data_attr)) {
name = text_element.attr(data_attr).toLowerCase();
}
if (text.includes(text_filter) || name.includes(text_filter)) {
$elements.eq(i).css('display', '');
} else {
$elements.eq(i).css('display', 'none');
}
}
});
},
deep_equal(a, b) {
return deep_equal(a, b);
},
@ -777,7 +802,7 @@ Object.assign(frappe.utils, {
name: M[0],
version: M[1],
};
},
}
});
// Array de duplicate

View file

@ -24,6 +24,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
this.setup_dashboard_page();
this.setup_dashboard_customization();
this.make_dashboard();
this.setup_events();
}
setup_dashboard_customization() {
@ -43,13 +44,11 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
<div class="text-muted uppercase">${dashboard_name}</div>
<div class="text-muted customize-dashboard" data-action="customize">${__('Customize')}</div>
<div class="small text-muted customize-options small-bounce">
<span class="reset-customization" data-action="reset_dashboard_customization">
${__('Reset')}
</span> / <span class="save-customization" data-action="save_dashboard_customization">
${__('Save')}
</span> / <span class="discard-customization" data-action="discard_dashboard_customization">
${__('Discard')}
</span>
<span class="reset-customization customize-option" data-action="reset_dashboard_customization">${__('Reset')}</span>
<span> / </span>
<span class="save-customization customize-option" data-action="save_dashboard_customization">${__('Save')}</span>
<span> / </span>
<span class="discard-customization customize-option" data-action="discard_dashboard_customization">${__('Discard')}</span>
</div>
</div>`);
@ -105,6 +104,11 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
}
}
setup_events() {
$(document.body).on('toggleFullWidth', () => this.render_dashboard());
$(document.body).on('toggleListSidebar', () => this.render_dashboard());
}
fetch_dashboard_items(doctype, filters, obj_name) {
return frappe.db.get_list(doctype, {
filters: filters,
@ -188,9 +192,8 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
}
this.toggle_customize(true);
this.chart_group.in_customize_mode = true;
this.in_customize_mode = true;
this.chart_group.customize();
this.number_cards.in_customize_mode = true;
this.number_card_group.customize();
}
@ -225,12 +228,14 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
}
reset_dashboard_customization() {
this.dashboard_settings = null;
frappe.model.user_settings.save(
this.doctype, 'dashboard_settings', this.dashboard_settings
).then(() => this.make_dashboard());
frappe.confirm(__("Are you sure you want to reset all customizations?"), () => {
this.dashboard_settings = null;
frappe.model.user_settings.save(
this.doctype, 'dashboard_settings', this.dashboard_settings
).then(() => this.make_dashboard());
this.toggle_customize(false);
this.toggle_customize(false);
});
}
toggle_customize(show) {

Some files were not shown because too many files have changed in this diff Show more