diff --git a/.eslintrc b/.eslintrc index 86abea34ce..0a76fdf2aa 100644 --- a/.eslintrc +++ b/.eslintrc @@ -116,6 +116,7 @@ "md5": true, "$": true, "jQuery": true, + "Vue": true, "moment": true, "hljs": true, "Awesomplete": true, diff --git a/bandit.yml b/bandit.yml index fce28629e8..b8560e97c8 100644 --- a/bandit.yml +++ b/bandit.yml @@ -1 +1 @@ -skips: ['B605', 'B404', 'B603', 'B607'] \ No newline at end of file +skips: ['E0203', 'B605', 'B404', 'B603', 'B607'] \ No newline at end of file diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js index 44dbde8fcf..4b2ae0da93 100644 --- a/cypress/integration/awesome_bar.js +++ b/cypress/integration/awesome_bar.js @@ -8,13 +8,6 @@ context('Awesome Bar', () => { cy.get('.navbar-home').click(); }); - it('navigates to modules', () => { - cy.get('#navbar-search') - .type('modules{downarrow}{enter}', { delay: 100 }); - - cy.location('hash').should('eq', '#modules'); - }); - it('navigates to doctype list', () => { cy.get('#navbar-search') .type('todo{downarrow}{enter}', { delay: 100 }); diff --git a/cypress/integration/relative_filters.js b/cypress/integration/relative_filters.js index efc6b930b2..d79e848994 100644 --- a/cypress/integration/relative_filters.js +++ b/cypress/integration/relative_filters.js @@ -21,7 +21,7 @@ context('Relative Timeframe', () => { cy.server(); cy.route({ method: 'POST', - url: '/' + url: '/api/method/frappe.desk.reportview.get' }).as('applyFilter'); cy.get('.filter-box .btn:contains("Apply")').click(); cy.wait('@applyFilter'); @@ -39,7 +39,7 @@ context('Relative Timeframe', () => { cy.server(); cy.route({ method: 'POST', - url: '/' + url: '/api/method/frappe.desk.reportview.get' }).as('applyFilter'); cy.get('.filter-box .btn:contains("Apply")').click(); cy.wait('@applyFilter'); diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js index 67b522c364..a083e514ba 100644 --- a/cypress/integration/table_multiselect.js +++ b/cypress/integration/table_multiselect.js @@ -3,9 +3,11 @@ context('Table MultiSelect', () => { cy.login('Administrator', 'qwe'); }); + let todo_description = 'table multiselect' + Math.random().toString().slice(2, 8); + it('select value from multiselect dropdown', () => { cy.visit('/desk#Form/ToDo/New ToDo 1'); - cy.fill_field('description', 'asdf', 'Text Editor').blur(); + cy.fill_field('description', todo_description, 'Text Editor').blur(); cy.get('input[data-fieldname="assign_to"]').focus().as('input'); cy.get('input[data-fieldname="assign_to"] + ul').should('be.visible'); cy.get('@input').type('faris{enter}', { delay: 100 }); @@ -14,7 +16,7 @@ context('Table MultiSelect', () => { cy.get('@selected-value').should('contain', 'faris@erpnext.com'); cy.server(); - cy.route('POST', '/').as('save_form'); + cy.route('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form'); // trigger save cy.get('.primary-action').click(); cy.wait('@save_form').its('status').should('eq', 200); @@ -23,8 +25,7 @@ context('Table MultiSelect', () => { it('delete value using backspace', () => { cy.visit('/desk#List/ToDo/List'); - cy.get('.list-row a').should('exist'); - cy.get('.list-subject').last().find('a').click(); + cy.get(`.list-subject:contains("table multiselect")`).last().find('a').click(); cy.get('input[data-fieldname="assign_to"]').focus().type('{backspace}'); cy.get('.frappe-control[data-fieldname="assign_to"] .form-control .tb-selected-value') .should('not.exist'); @@ -32,8 +33,7 @@ context('Table MultiSelect', () => { it('delete value using x', () => { cy.visit('/desk#List/ToDo/List'); - cy.get('.list-row a').should('exist'); - cy.get('.list-subject').last().find('a').click(); + cy.get(`.list-subject:contains("table multiselect")`).last().find('a').click(); cy.get('.frappe-control[data-fieldname="assign_to"] .form-control .tb-selected-value').as('existing_value'); cy.get('@existing_value').find('.btn-remove').click(); cy.get('@existing_value').should('not.exist'); @@ -41,8 +41,7 @@ context('Table MultiSelect', () => { it('navigate to selected value', () => { cy.visit('/desk#List/ToDo/List'); - cy.get('.list-row a').should('exist'); - cy.get('.list-subject').last().find('a').click(); + cy.get(`.list-subject:contains("table multiselect")`).last().find('a').click(); cy.get('.frappe-control[data-fieldname="assign_to"] .form-control .tb-selected-value').as('existing_value'); cy.get('@existing_value').find('.btn-link-to-form').click(); cy.location('hash').should('contain', 'Form/User/faris@erpnext.com'); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index f25cc12ea6..36df14d1b0 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -49,4 +49,8 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => { } else { return cy.get('@input').type(value); } -}); \ No newline at end of file +}); + +Cypress.Commands.add('awesomebar', (text) => { + cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 }); +}); diff --git a/frappe/.pylintrc b/frappe/.pylintrc new file mode 100644 index 0000000000..4b2ea0a564 --- /dev/null +++ b/frappe/.pylintrc @@ -0,0 +1 @@ +disable=access-member-before-definition \ No newline at end of file diff --git a/frappe/__init__.py b/frappe/__init__.py index f6aa7f9e1d..a1c3ebf34d 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -16,6 +16,7 @@ from faker import Faker # public from .exceptions import * from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader) +from .utils.error import get_frame_locals # Hamless for Python 3 # For Python 2 set default encoding to utf-8 @@ -23,7 +24,7 @@ if sys.version[0] == '2': reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '11.1.3' +__version__ = '11.1.8' __title__ = "Frappe Framework" local = Local() @@ -273,7 +274,8 @@ def errprint(msg): if not request or (not "cmd" in local.form_dict) or conf.developer_mode: print(msg) - error_log.append(msg) + from .utils import escape_html + error_log.append({"exc": escape_html(msg), "locals": get_frame_locals()}) def log(msg): """Add to `debug_log`. @@ -501,6 +503,7 @@ def read_only(): retval = fn(*args, **get_newargs(fn, kwargs)) if local and hasattr(local, 'master_db'): + local.db.close() local.db = local.master_db return retval @@ -916,11 +919,15 @@ def get_hooks(hook=None, default=None, app_name=None): append_hook(hooks, key, getattr(app_hooks, key)) return hooks + no_cache = conf.developer_mode or False if app_name: hooks = _dict(load_app_hooks(app_name)) else: - hooks = _dict(cache().get_value("app_hooks", load_app_hooks)) + if no_cache: + hooks = _dict(load_app_hooks()) + else: + hooks = _dict(cache().get_value("app_hooks", load_app_hooks)) if hook: return hooks.get(hook) or (default if default is not None else []) diff --git a/frappe/app.py b/frappe/app.py index bc4ecc9b2d..9da7ea71a0 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -22,8 +22,9 @@ import frappe.website.render from frappe.utils import get_site_name from frappe.middlewares import StaticDataMiddleware from frappe.utils.error import make_error_snapshot -from frappe.core.doctype.communication.comment import update_comments_in_parent_after_request +from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request from frappe import _ +import frappe.recorder local_manager = LocalManager([frappe.local]) @@ -41,7 +42,6 @@ class RequestContext(object): def __exit__(self, type, value, traceback): frappe.destroy() - @Request.application def application(request): response = None @@ -51,6 +51,8 @@ def application(request): init_request(request) + frappe.recorder.record() + if frappe.local.form_dict.cmd: response = frappe.handler.handle() @@ -91,6 +93,8 @@ def application(request): if response and hasattr(frappe.local, 'cookie_manager'): frappe.local.cookie_manager.flush_cookies(response=response) + frappe.recorder.dump() + frappe.destroy() return response diff --git a/frappe/boot.py b/frappe/boot.py index 18fb080d4f..043c1b0361 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -96,8 +96,8 @@ def load_conf_settings(bootinfo): if key in conf: bootinfo[key] = conf.get(key) def load_desktop_icons(bootinfo): - from frappe.desk.doctype.desktop_icon.desktop_icon import get_desktop_icons - bootinfo.desktop_icons = get_desktop_icons() + from frappe.config import get_modules_from_all_apps_for_user + bootinfo.allowed_modules = get_modules_from_all_apps_for_user() def get_allowed_pages(): return get_user_pages_or_reports('Page') diff --git a/frappe/chat/doctype/chat_profile/chat_profile.py b/frappe/chat/doctype/chat_profile/chat_profile.py index 22ce94858d..283494de85 100644 --- a/frappe/chat/doctype/chat_profile/chat_profile.py +++ b/frappe/chat/doctype/chat_profile/chat_profile.py @@ -9,99 +9,94 @@ import frappe from frappe.core.doctype.version.version import get_diff from frappe.chat.doctype.chat_room import chat_room from frappe.chat.util import ( - safe_json_loads, - filter_dict, - dictify + safe_json_loads, + filter_dict, + dictify ) session = frappe.session class ChatProfile(Document): - def before_save(self): - if not self.is_new(): - self.get_doc_before_save() + def before_save(self): + if not self.is_new(): + self.get_doc_before_save() - def on_update(self): - if not self.is_new(): - b, a = self.get_doc_before_save(), self - diff = dictify(get_diff(a, b)) - if diff: - user = session.user + def on_update(self): + if not self.is_new(): + b, a = self.get_doc_before_save(), self + diff = dictify(get_diff(a, b)) + if diff: + user = session.user - fields = [changed[0] for changed in diff.changed] + fields = [changed[0] for changed in diff.changed] - if 'status' in fields: - rooms = chat_room.get(user, filters = ['Chat Room', 'type', '=', 'Direct']) - update = dict(user = user, data = dict(status = self.status)) + if 'status' in fields: + rooms = chat_room.get(user, filters = ['Chat Room', 'type', '=', 'Direct']) + update = dict(user = user, data = dict(status = self.status)) - for room in rooms: - frappe.publish_realtime('frappe.chat.profile:update', update, room = room.name, after_commit = True) + for room in rooms: + frappe.publish_realtime('frappe.chat.profile:update', update, room = room.name, after_commit = True) - if 'enable_chat' in fields: - update = dict(user = user, data = dict(enable_chat = bool(self.enable_chat))) - frappe.publish_realtime('frappe.chat.profile:update', update, user = user, after_commit = True) + if 'enable_chat' in fields: + update = dict(user = user, data = dict(enable_chat = bool(self.enable_chat))) + frappe.publish_realtime('frappe.chat.profile:update', update, user = user, after_commit = True) def authenticate(user): - if user != session.user: - frappe.throw(_("Sorry, you're not authorized.")) + if user != session.user: + frappe.throw(_("Sorry, you're not authorized.")) @frappe.whitelist() def get(user, fields = None): - duser = frappe.get_doc('User', user) - dprof = frappe.get_doc('Chat Profile', user) + duser = frappe.get_doc('User', user) + dprof = frappe.get_doc('Chat Profile', user) - # If you're adding something here, make sure the client recieves it. - profile = dict( - # User - name = duser.name, - email = duser.email, - first_name = duser.first_name, - last_name = duser.last_name, - username = duser.username, - avatar = duser.user_image, - bio = duser.bio, - # Chat Profile - status = dprof.status, - chat_background = dprof.chat_background, - message_preview = bool(dprof.message_preview), - notification_tones = bool(dprof.notification_tones), - conversation_tones = bool(dprof.conversation_tones), - enable_chat = bool(dprof.enable_chat) - ) - profile = filter_dict(profile, fields) + # If you're adding something here, make sure the client recieves it. + profile = dict( + # User + name = duser.name, + email = duser.email, + first_name = duser.first_name, + last_name = duser.last_name, + username = duser.username, + avatar = duser.user_image, + bio = duser.bio, + # Chat Profile + status = dprof.status, + chat_background = dprof.chat_background, + message_preview = bool(dprof.message_preview), + notification_tones = bool(dprof.notification_tones), + conversation_tones = bool(dprof.conversation_tones), + enable_chat = bool(dprof.enable_chat) + ) + profile = filter_dict(profile, fields) - return dictify(profile) + return dictify(profile) @frappe.whitelist() def create(user, exists_ok = False, fields = None): - authenticate(user) + authenticate(user) - exists_ok, fields = safe_json_loads(exists_ok, fields) + exists_ok, fields = safe_json_loads(exists_ok, fields) - result = frappe.db.sql(""" - SELECT * - FROM `tabChat Profile` - WHERE `user` = '{user}' - """.format(user = user)) + try: + dprof = frappe.new_doc('Chat Profile') + dprof.user = user + dprof.save(ignore_permissions = True) + except frappe.DuplicateEntryError: + frappe.clear_messages() + if not exists_ok: + frappe.throw(_('Chat Profile for User {0} exists.').format(user)) - if result: - if not exists_ok: - frappe.throw(_('Chat Profile for User {0} exists.').format(user)) - else: - dprof = frappe.new_doc('Chat Profile') - dprof.user = user - dprof.save(ignore_permissions = True) + profile = get(user, fields = fields) - profile = get(user, fields = fields) - - return profile + return profile @frappe.whitelist() def update(user, data): - authenticate(user) + authenticate(user) - data = safe_json_loads(data) + data = safe_json_loads(data) - dprof = frappe.get_doc('Chat Profile', user) - dprof.update(data) - dprof.save(ignore_permissions = True) \ No newline at end of file + dprof = frappe.get_doc('Chat Profile', user) + dprof.update(data) + dprof.save(ignore_permissions = True) \ No newline at end of file diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 5634e6108d..3798c07e91 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -569,6 +569,23 @@ def browse(context, site): else: click.echo("\nSite named \033[1m{}\033[0m doesn't exist\n".format(site)) + +@click.command('start-recording') +@pass_context +def start_recording(context): + for site in context.sites: + frappe.init(site=site) + frappe.recorder.start() + + +@click.command('stop-recording') +@pass_context +def stop_recording(context): + for site in context.sites: + frappe.init(site=site) + frappe.recorder.stop() + + commands = [ add_system_manager, backup, @@ -592,5 +609,7 @@ commands = [ _use, set_last_active_for_user, publish_realtime, - browse + browse, + start_recording, + stop_recording, ] diff --git a/frappe/config/__init__.py b/frappe/config/__init__.py index e69de29bb2..34bc86dbd6 100644 --- a/frappe/config/__init__.py +++ b/frappe/config/__init__.py @@ -0,0 +1,89 @@ +from __future__ import unicode_literals +from frappe import _ +import frappe +from frappe.desk.moduleview import get_data +from six import iteritems + +def get_modules_from_all_apps_for_user(user=None): + if not user: + user = frappe.session.user + + all_modules = get_modules_from_all_apps() + user_blocked_modules = frappe.get_doc('User', user).get_blocked_modules() + + allowed_modules_list = [m for m in all_modules if m.get("module_name") not in user_blocked_modules] + + return allowed_modules_list + +def get_modules_from_all_apps(): + modules_list = [] + for app in frappe.get_installed_apps(): + modules_list += get_modules_from_app(app) + return modules_list + +def get_modules_from_app(app): + try: + modules = frappe.get_attr(app + '.config.desktop.get_data')() or {} + except ImportError: + return [] + + active_domains = frappe.get_active_domains() + + if isinstance(modules, dict): + active_modules_list = [] + for m, module in iteritems(modules): + module['module_name'] = m + active_modules_list.append(module) + else: + for m in modules: + if m.get("type") == "module" and "category" not in m: + m["category"] = "Modules" + + # Only newly formatted modules that have a category to be shown on desk + modules = [m for m in modules if m.get("category")] + active_modules_list = [] + + for m in modules: + to_add = True + module_name = m.get("module_name") + + # Check Domain + if is_domain(m): + if module_name not in active_domains: + to_add = False + + if "condition" in m and not m["condition"]: + to_add = False + + if to_add: + onboard_present = is_onboard_present(m) if show_onboard(m) else False + m["onboard_present"] = onboard_present + active_modules_list.append(m) + + return active_modules_list + +@frappe.whitelist() +def is_onboard_present(module): + exists_cache = {} + def exists(name, link_type): + exists = exists_cache.get(name) + if not exists: + if link_type == "doctype" and not frappe.db.get_value('DocType', name, 'issingle'): + exists = frappe.db.count(name) + else: + exists = True + exists_cache[name] = exists + return exists + + sections = get_data(module["module_name"], False) + for section in sections: + for item in section["items"]: + if exists(item.get("name"), item.get("type")): + return True + return False + +def show_onboard(module): + return module.get("type") == "module" + +def is_domain(module): + return module.get("category") == "Domains" diff --git a/frappe/config/customization.py b/frappe/config/customization.py new file mode 100644 index 0000000000..1ecf039f88 --- /dev/null +++ b/frappe/config/customization.py @@ -0,0 +1,44 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return [ + { + "label": _("Customize"), + "icon": "fa fa-glass", + "items": [ + { + "type": "doctype", + "name": "Customize Form", + "description": _("Change field properties (hide, readonly, permission etc.)") + }, + { + "type": "doctype", + "name": "Custom Field", + "description": _("Add fields to forms.") + }, + { + "type": "doctype", + "label": _("Custom Translations"), + "name": "Translation", + "description": _("Add your own translations") + }, + { + "type": "doctype", + "name": "Custom Script", + "description": _("Add custom javascript to forms.") + }, + { + "type": "doctype", + "name": "DocType", + "description": _("Add custom forms.") + }, + { + "type": "doctype", + "label": _("Custom Tags"), + "name": "Tag Category", + "description": _("Add your own Tag Categories") + } + ] + }, + ] diff --git a/frappe/config/desk.py b/frappe/config/desk.py index 8efe293411..40db97ef8c 100644 --- a/frappe/config/desk.py +++ b/frappe/config/desk.py @@ -12,11 +12,7 @@ def get_data(): "name": "ToDo", "label": _("To Do"), "description": _("Documents assigned to you and by you."), - }, - { - "type": "doctype", - "name": "File", - "label": _("Files"), + "onboard": 1, }, { "type": "doctype", @@ -24,6 +20,18 @@ def get_data(): "label": _("Calendar"), "link": "List/Event/Calendar", "description": _("Event and other calendars."), + "onboard": 1, + }, + { + "type": "doctype", + "name": "Note", + "description": _("Private and public Notes."), + "onboard": 1, + }, + { + "type": "doctype", + "name": "File", + "label": _("Files"), }, { "type": "page", @@ -32,11 +40,6 @@ def get_data(): "description": _("Chat messages and other notifications."), "data_doctype": "Communication" }, - { - "type": "doctype", - "name": "Note", - "description": _("Private and public Notes."), - }, { "type": "page", "label": _("Activity"), @@ -52,6 +55,7 @@ def get_data(): "type": "doctype", "name": "Newsletter", "description": _("Newsletters to contacts, leads."), + "onboard": 1, }, { "type": "doctype", diff --git a/frappe/config/desktop.py b/frappe/config/desktop.py index b620508c06..a4677a906b 100644 --- a/frappe/config/desktop.py +++ b/frappe/config/desktop.py @@ -1,91 +1,109 @@ from __future__ import unicode_literals +import frappe from frappe import _ def get_data(): return [ + # Administration { "module_name": "Desk", + "category": "Administration", "label": _("Tools"), "color": "#FFF5A7", "reverse": 1, "icon": "octicon octicon-calendar", - "type": "module" - }, - { - "module_name": "File Manager", - "color": "#AA784D", - "doctype": "File", - "icon": "octicon octicon-file-directory", - "label": _("File Manager"), - "link": "List/File", - "type": "list", - "hidden": 1 - }, - { - "module_name": "Website", - "color": "#16a085", - "icon": "octicon octicon-globe", "type": "module", - "hidden": 1 + "description": "Todos, notes, calendar and newsletter." }, { - "module_name": "Integrations", - "color": "#16a085", - "icon": "octicon octicon-globe", - "type": "module", - "hidden": 1 - }, - { - "module_name": "Setup", + "module_name": "Settings", + "category": "Administration", + "label": _("Settings"), "color": "#bdc3c7", "reverse": 1, "icon": "octicon octicon-settings", "type": "module", - "hidden": 1 + "hidden": 1, + "description": "Data import, printing, email and workflows." }, { - "module_name": 'Email Inbox', - "type": 'list', - "label": 'Email Inbox', - "_label": _('Email Inbox'), - "_id": 'Email Inbox', - "_doctype": 'Communication', - "icon": 'fa fa-envelope-o', - "color": '#589494', - "link": 'List/Communication/Inbox' + "module_name": "Users and Permissions", + "category": "Administration", + "label": _("Users and Permissions"), + "color": "#bdc3c7", + "reverse": 1, + "icon": "octicon octicon-settings", + "type": "module", + "hidden": 1, + "description": "Setup roles and permissions for users on documents." + }, + { + "module_name": "Customization", + "category": "Administration", + "label": _("Customization"), + "color": "#bdc3c7", + "reverse": 1, + "icon": "octicon octicon-settings", + "type": "module", + "hidden": 1, + "description": "Customize forms, custom fields, scripts and translations." + }, + { + "module_name": "Integrations", + "category": "Administration", + "label": _("Integrations"), + "color": "#16a085", + "icon": "octicon octicon-globe", + "type": "module", + "hidden": 1, + "description": "DropBox, Woocomerce, AWS, Shopify and GoCardless." + }, + { + "module_name": 'Contacts', + "category": "Administration", + "label": _("Contacts"), + "type": 'module', + "icon": "octicon octicon-book", + "color": '#ffaedb', + "hidden": 1, + "description": "People Contacts and Address Book." }, { "module_name": "Core", - "label": "Developer", + "category": "Administration", "_label": _("Developer"), + "label": "Developer", "color": "#589494", "icon": "octicon octicon-circuit-board", "type": "module", "system_manager": 1, - "hidden": 1 - }, - { - "module_name": 'Contacts', - "type": 'module', - "icon": "octicon octicon-book", - "color": '#FFAEDB', + "condition": getattr(frappe.local.conf, 'developer_mode', 0), "hidden": 1, + "description": "Doctypes, dev tools and logs." + }, + + # Places + { + "module_name": "Website", + "category": "Places", + "label": _("Website"), + "_label": _("Website"), + "color": "#16a085", + "icon": "octicon octicon-globe", + "type": "module", + "hidden": 1, + "description": "Webpages, webforms, blogs and website theme." }, { "module_name": 'Social', + "category": "Places", "label": _('Social'), "icon": "octicon octicon-heart", "type": 'link', - "link": 'social/home', + "link": '#social/home', "color": '#FF4136', 'standard': 1, - 'idx': 15 + 'idx': 15, + "description": "Build your profile and share posts with other users." }, - { - "module_name": 'Settings', - "color": "#bdc3c7", - "reverse": 1, - "icon": "octicon octicon-settings", - "type": "module" - } ] diff --git a/frappe/config/settings.py b/frappe/config/settings.py index d1c00c2e71..75c8972feb 100644 --- a/frappe/config/settings.py +++ b/frappe/config/settings.py @@ -1,51 +1,188 @@ +from __future__ import unicode_literals from frappe import _ +from frappe.desk.moduleview import add_setup_section def get_data(): - return [{ - "label": _("Settings"), - "icon": "fa fa-wrench", - "items": [ - { - "type": "doctype", - "name": "System Settings", - "label": _("System Settings"), - "description": _("Language, Date and Time settings"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Domain Settings", - "label": _("Domain Settings"), - "description": _("Enable / Disable Domains"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Print Settings", - "label": _("Print Settings"), - "description": _("Print Style, PDF Size"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Website Settings", - "label": _("Website Settings"), - "description": _("Landing Page, Website Theme, Brand Setup and more"), - "hide_count": True - }, - { - "type": "doctype", - "name": "S3 Backup Settings", - "label": _("S3 Backup Settings"), - "description": _("Enable / Disable Backup, Backup Frequency"), - "hide_count": True - }, - { - "type": "doctype", - "name": "SMS Settings", - "label": _("SMS Settings"), - "description": _("SMS Gateway URL, Message & Receiver Parameter"), - "hide_count": True - } - ] - }] + data = [ + { + "label": _("Core"), + "icon": "fa fa-wrench", + "items": [ + { + "type": "doctype", + "name": "System Settings", + "label": _("System Settings"), + "description": _("Language, Date and Time settings"), + "hide_count": True + }, + { + "type": "doctype", + "name": "Error Log", + "description": _("Log of error on automated events (scheduler).") + }, + { + "type": "doctype", + "name": "Error Snapshot", + "description": _("Log of error during requests.") + }, + { + "type": "doctype", + "name": "Domain Settings", + "label": _("Domain Settings"), + "description": _("Enable / Disable Domains"), + "hide_count": True + }, + ] + }, + { + "label": _("Data"), + "icon": "fa fa-th", + "items": [ + { + "type": "doctype", + "name": "Data Import", + "label": _("Import Data"), + "icon": "octicon octicon-cloud-upload", + "description": _("Import Data from CSV / Excel files.") + }, + { + "type": "doctype", + "name": "Data Export", + "label": _("Export Data"), + "icon": "octicon octicon-cloud-upload", + "description": _("Export Data in CSV / Excel format.") + }, + { + "type": "doctype", + "name": "Naming Series", + "description": _("Set numbering series for transactions."), + "hide_count": True + }, + { + "type": "doctype", + "name": "Rename Tool", + "label": _("Bulk Rename"), + "description": _("Rename many items by uploading a .csv file."), + "hide_count": True + }, + { + "type": "doctype", + "name": "Bulk Update", + "label": _("Bulk Update"), + "description": _("Update many values at one time."), + "hide_count": True + }, + { + "type": "page", + "name": "backups", + "label": _("Download Backups"), + "description": _("List of backups available for download"), + "icon": "fa fa-download" + }, + { + "type": "doctype", + "name": "Deleted Document", + "label": _("Deleted Documents"), + "description": _("Restore or permanently delete a document.") + }, + ] + }, + { + "label": _("Email"), + "icon": "fa fa-envelope", + "items": [ + { + "type": "doctype", + "name": "Email Account", + "description": _("Add / Manage Email Accounts.") + }, + { + "type": "doctype", + "name": "Email Domain", + "description": _("Add / Manage Email Domains.") + }, + { + "type": "doctype", + "name": "Notification", + "description": _("Setup Notifications based on various criteria.") + }, + { + "type": "doctype", + "name": "Email Template", + "description": _("Email Templates for common queries.") + }, + { + "type": "doctype", + "name": "Auto Email Report", + "description": _("Setup Reports to be emailed at regular intervals"), + }, + { + "type": "doctype", + "name": "Newsletter", + "description": _("Create and manage newsletter") + } + ] + }, + { + "label": _("Printing"), + "icon": "fa fa-print", + "items": [ + { + "type": "page", + "label": _("Print Format Builder"), + "name": "print-format-builder", + "description": _("Drag and Drop tool to build and customize Print Formats.") + }, + { + "type": "doctype", + "name": "Print Settings", + "description": _("Set default format, page size, print style etc.") + }, + { + "type": "doctype", + "name": "Print Format", + "description": _("Customized HTML Templates for printing transactions.") + }, + { + "type": "doctype", + "name": "Print Style", + "description": _("Stylesheets for Print Formats") + }, + ] + }, + { + "label": _("Workflow"), + "icon": "fa fa-random", + "items": [ + { + "type": "doctype", + "name": "Workflow", + "description": _("Define workflows for forms.") + }, + { + "type": "doctype", + "name": "Workflow State", + "description": _("States for workflow (e.g. Draft, Approved, Cancelled).") + }, + { + "type": "doctype", + "name": "Workflow Action", + "description": _("Actions for workflow (e.g. Approve, Cancel).") + }, + ] + }, + { + "label": _("Applications"), + "items":[ + { + "type": "page", + "name": "applications", + "label": _("Application Installer"), + "description": _("Install Applications."), + "icon": "fa fa-download" + }, + ] + } + ] + add_setup_section(data, "frappe", "website", _("Website"), "fa fa-globe") + return data diff --git a/frappe/config/setup.py b/frappe/config/setup.py deleted file mode 100644 index 7ed9d837ce..0000000000 --- a/frappe/config/setup.py +++ /dev/null @@ -1,289 +0,0 @@ -from __future__ import unicode_literals -from frappe import _ -from frappe.desk.moduleview import add_setup_section - -def get_data(): - data = [ - { - "label": _("Users"), - "icon": "fa fa-group", - "items": [ - { - "type": "doctype", - "name": "User", - "description": _("System and Website Users") - }, - { - "type": "doctype", - "name": "Role", - "description": _("User Roles") - }, - { - "type": "doctype", - "name": "Role Profile", - "description": _("Role Profile") - } - ] - }, - { - "label": _("Permissions"), - "icon": "fa fa-lock", - "items": [ - { - "type": "page", - "name": "permission-manager", - "label": _("Role Permissions Manager"), - "icon": "fa fa-lock", - "description": _("Set Permissions on Document Types and Roles") - }, - { - "type": "doctype", - "name": "User Permission", - "label": _("User Permissions"), - "icon": "fa fa-lock", - "description": _("Restrict user for specific document") - }, - { - "type": "doctype", - "name": "Role Permission for Page and Report", - "description": _("Set custom roles for page and report") - }, - { - "type": "report", - "is_query_report": True, - "doctype": "User", - "icon": "fa fa-eye-open", - "name": "Permitted Documents For User", - "description": _("Check which Documents are readable by a User") - }, - { - "type": "report", - "doctype": "DocShare", - "icon": "fa fa-share", - "name": "Document Share Report", - "description": _("Report of all document shares") - } - ] - }, - { - "label": _("Settings"), - "icon": "fa fa-wrench", - "items": [ - { - "type": "doctype", - "name": "System Settings", - "label": _("System Settings"), - "description": _("Language, Date and Time settings"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Error Log", - "description": _("Log of error on automated events (scheduler).") - }, - { - "type": "doctype", - "name": "Error Snapshot", - "description": _("Log of error during requests.") - }, - { - "type": "doctype", - "name": "Domain Settings", - "label": _("Domain Settings"), - "description": _("Enable / Disable Domains"), - "hide_count": True - }, - ] - }, - { - "label": _("Data"), - "icon": "fa fa-th", - "items": [ - { - "type": "doctype", - "name": "Data Import", - "label": _("Import Data"), - "icon": "octicon octicon-cloud-upload", - "description": _("Import Data from CSV / Excel files.") - }, - { - "type": "doctype", - "name": "Data Export", - "label": _("Export Data"), - "icon": "octicon octicon-cloud-upload", - "description": _("Export Data in CSV / Excel format.") - }, - { - "type": "doctype", - "name": "Naming Series", - "description": _("Set numbering series for transactions."), - "hide_count": True - }, - { - "type": "doctype", - "name": "Rename Tool", - "label": _("Bulk Rename"), - "description": _("Rename many items by uploading a .csv file."), - "hide_count": True - }, - { - "type": "doctype", - "name": "Bulk Update", - "label": _("Bulk Update"), - "description": _("Update many values at one time."), - "hide_count": True - }, - { - "type": "page", - "name": "backups", - "label": _("Download Backups"), - "description": _("List of backups available for download"), - "icon": "fa fa-download" - }, - { - "type": "doctype", - "name": "Deleted Document", - "label": _("Deleted Documents"), - "description": _("Restore or permanently delete a document.") - }, - ] - }, - { - "label": _("Email"), - "icon": "fa fa-envelope", - "items": [ - { - "type": "doctype", - "name": "Email Account", - "description": _("Add / Manage Email Accounts.") - }, - { - "type": "doctype", - "name": "Email Domain", - "description": _("Add / Manage Email Domains.") - }, - { - "type": "doctype", - "name": "Notification", - "description": _("Setup Notifications based on various criteria.") - }, - { - "type": "doctype", - "name": "Email Template", - "description": _("Email Templates for common queries.") - }, - { - "type": "doctype", - "name": "Auto Email Report", - "description": _("Setup Reports to be emailed at regular intervals"), - }, - { - "type": "doctype", - "name": "Newsletter", - "description": _("Create and manage newsletter") - } - ] - }, - { - "label": _("Printing"), - "icon": "fa fa-print", - "items": [ - { - "type": "page", - "label": _("Print Format Builder"), - "name": "print-format-builder", - "description": _("Drag and Drop tool to build and customize Print Formats.") - }, - { - "type": "doctype", - "name": "Print Settings", - "description": _("Set default format, page size, print style etc.") - }, - { - "type": "doctype", - "name": "Print Format", - "description": _("Customized HTML Templates for printing transactions.") - }, - { - "type": "doctype", - "name": "Print Style", - "description": _("Stylesheets for Print Formats") - }, - ] - }, - { - "label": _("Workflow"), - "icon": "fa fa-random", - "items": [ - { - "type": "doctype", - "name": "Workflow", - "description": _("Define workflows for forms.") - }, - { - "type": "doctype", - "name": "Workflow State", - "description": _("States for workflow (e.g. Draft, Approved, Cancelled).") - }, - { - "type": "doctype", - "name": "Workflow Action", - "description": _("Actions for workflow (e.g. Approve, Cancel).") - }, - ] - }, - { - "label": _("Customize"), - "icon": "fa fa-glass", - "items": [ - { - "type": "doctype", - "name": "Customize Form", - "description": _("Change field properties (hide, readonly, permission etc.)"), - "hide_count": True - }, - { - "type": "doctype", - "name": "Custom Field", - "description": _("Add fields to forms.") - }, - { - "type": "doctype", - "label": _("Custom Translations"), - "name": "Translation", - "description": _("Add your own translations") - }, - { - "type": "doctype", - "name": "Custom Script", - "description": _("Add custom javascript to forms.") - }, - { - "type": "doctype", - "name": "DocType", - "description": _("Add custom forms.") - }, - { - "type": "doctype", - "label": _("Custom Tags"), - "name": "Tag Category", - "description": _("Add your own Tag Categories") - } - - ] - }, - { - "label": _("Applications"), - "items":[ - { - "type": "page", - "name": "applications", - "label": _("Application Installer"), - "description": _("Install Applications."), - "icon": "fa fa-download" - }, - ] - } - ] - add_setup_section(data, "frappe", "website", _("Website"), "fa fa-globe") - return data diff --git a/frappe/config/users_and_permissions.py b/frappe/config/users_and_permissions.py new file mode 100644 index 0000000000..6f739d43ce --- /dev/null +++ b/frappe/config/users_and_permissions.py @@ -0,0 +1,67 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return [ + { + "label": _("Users"), + "icon": "fa fa-group", + "items": [ + { + "type": "doctype", + "name": "User", + "description": _("System and Website Users") + }, + { + "type": "doctype", + "name": "Role", + "description": _("User Roles") + }, + { + "type": "doctype", + "name": "Role Profile", + "description": _("Role Profile") + } + ] + }, + { + "label": _("Permissions"), + "icon": "fa fa-lock", + "items": [ + { + "type": "page", + "name": "permission-manager", + "label": _("Role Permissions Manager"), + "icon": "fa fa-lock", + "description": _("Set Permissions on Document Types and Roles") + }, + { + "type": "doctype", + "name": "User Permission", + "label": _("User Permissions"), + "icon": "fa fa-lock", + "description": _("Restrict user for specific document") + }, + { + "type": "doctype", + "name": "Role Permission for Page and Report", + "description": _("Set custom roles for page and report") + }, + { + "type": "report", + "is_query_report": True, + "doctype": "User", + "icon": "fa fa-eye-open", + "name": "Permitted Documents For User", + "description": _("Check which Documents are readable by a User") + }, + { + "type": "report", + "doctype": "DocShare", + "icon": "fa fa-share", + "name": "Document Share Report", + "description": _("Report of all document shares") + } + ] + }, + ] \ No newline at end of file diff --git a/frappe/config/website.py b/frappe/config/website.py index de66ca0959..2de27a581b 100644 --- a/frappe/config/website.py +++ b/frappe/config/website.py @@ -11,11 +11,13 @@ def get_data(): "type": "doctype", "name": "Web Page", "description": _("Content web page."), + "onboard": 1, }, { "type": "doctype", "name": "Web Form", "description": _("User editable form on Website."), + "onboard": 1, }, { "type": "doctype", @@ -35,6 +37,7 @@ def get_data(): "type": "doctype", "name": "Blog Post", "description": _("Single Post (article)."), + "onboard": 1, }, { "type": "doctype", @@ -56,11 +59,13 @@ def get_data(): "type": "doctype", "name": "Website Settings", "description": _("Setup of top navigation bar, footer and logo."), + "onboard": 1, }, { "type": "doctype", "name": "Website Theme", "description": _("List of themes for Website."), + "onboard": 1, }, { "type": "doctype", @@ -86,6 +91,7 @@ def get_data(): "type": "doctype", "name": "Portal Settings", "label": _("Portal Settings"), + "onboard": 1, } ] }, diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index 2ab680c765..38d292e9b4 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -101,12 +101,13 @@ def get_permitted_and_not_permitted_links(doctype): not_permitted_links = [] meta = frappe.get_meta(doctype) + allowed_doctypes = frappe.permissions.get_doctypes_with_read() for df in meta.get_link_fields(): if df.options not in ("Customer", "Supplier", "Company", "Sales Partner"): continue - if frappe.has_permission(df.options): + if df.options in allowed_doctypes: permitted_links.append(df) else: not_permitted_links.append(df) @@ -145,10 +146,9 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil _doctypes = tuple([d for d in _doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)]) all_doctypes = [d[0] for d in doctypes + _doctypes] - valid_doctypes = [] + allowed_doctypes = frappe.permissions.get_doctypes_with_read() - for doctype in all_doctypes: - if frappe.has_permission(doctype): - valid_doctypes.append([doctype]) + valid_doctypes = sorted(set(all_doctypes).intersection(set(allowed_doctypes))) + valid_doctypes = [[doctype] for doctype in valid_doctypes] - return sorted(valid_doctypes) + return valid_doctypes diff --git a/frappe/core/doctype/activity_log/feed.py b/frappe/core/doctype/activity_log/feed.py index b4d46eb5c1..e09436196e 100644 --- a/frappe/core/doctype/activity_log/feed.py +++ b/frappe/core/doctype/activity_log/feed.py @@ -56,10 +56,13 @@ def logout_feed(user, reason): subject = _("{0} logged out: {1}").format(get_fullname(user), frappe.bold(reason)) add_authentication_log(subject, user, operation="Logout") -def get_feed_match_conditions(user=None, force=True): +def get_feed_match_conditions(user=None, doctype='Comment'): if not user: user = frappe.session.user - conditions = ['`tabCommunication`.owner={user} or `tabCommunication`.reference_owner={user}'.format(user=frappe.db.escape(user))] + conditions = ['`tab{doctype}`.owner={user} or `tab{doctype}`.reference_owner={user}'.format( + user = frappe.db.escape(user), + doctype = doctype + )] user_permissions = frappe.permissions.get_user_permissions(user) can_read = frappe.get_user().get_can_read() @@ -68,9 +71,13 @@ def get_feed_match_conditions(user=None, force=True): list(set(can_read) - set(list(user_permissions)))] if can_read_doctypes: - conditions += ["""(`tabCommunication`.reference_doctype is null - or `tabCommunication`.reference_doctype = '' - or `tabCommunication`.reference_doctype in ({}))""".format(", ".join(can_read_doctypes))] + conditions += ["""(`tab{doctype}`.reference_doctype is null + or `tab{doctype}`.reference_doctype = '' + or `tab{doctype}`.reference_doctype + in ({values}))""".format( + doctype = doctype, + values =", ".join(can_read_doctypes) + )] if user_permissions: can_read_docs = [] @@ -79,7 +86,8 @@ def get_feed_match_conditions(user=None, force=True): can_read_docs.append('{}|{}'.format(doctype, frappe.db.escape(n.get('doc', '')))) if can_read_docs: - conditions.append("concat_ws('|', `tabCommunication`.reference_doctype, `tabCommunication`.reference_name) in ({})".format( - ", ".join(can_read_docs))) + conditions.append("concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format( + doctype = doctype, + values = ", ".join(can_read_docs))) - return "(" + " or ".join(conditions) + ")" + return "(" + " or ".join(conditions) + ")" diff --git a/frappe/desk/page/modules/__init__.py b/frappe/core/doctype/comment/__init__.py similarity index 100% rename from frappe/desk/page/modules/__init__.py rename to frappe/core/doctype/comment/__init__.py diff --git a/frappe/core/doctype/comment/comment.js b/frappe/core/doctype/comment/comment.js new file mode 100644 index 0000000000..a793f766cb --- /dev/null +++ b/frappe/core/doctype/comment/comment.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Comment', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/comment/comment.json b/frappe/core/doctype/comment/comment.json new file mode 100644 index 0000000000..344d6399b9 --- /dev/null +++ b/frappe/core/doctype/comment/comment.json @@ -0,0 +1,535 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2019-02-07 10:10:46.845678", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Comment", + "fieldname": "comment_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Comment Type", + "length": 0, + "no_copy": 0, + "options": "Comment\nLike\nInfo\nLabel\nWorkflow\nCreated\nSubmitted\nCancelled\nUpdated\nDeleted\nAssigned\nAssignment Completed\nAttachment\nAttachment Removed\nShared\nUnshared\nBot\nRelinked\nEdit", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "comment_email", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Comment Email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subject", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subject", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "comment_by", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Comment By", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "published", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Published", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "seen", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Seen", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_5", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Reference Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reference Name", + "length": 0, + "no_copy": 0, + "options": "reference_doctype", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "link_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Link DocType", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "link_name", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Link Name", + "length": 0, + "no_copy": 0, + "options": "link_doctype", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_owner", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reference Owner", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "content", + "fieldtype": "HTML Editor", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Content", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-02-08 09:18:33.843171", + "modified_by": "Administrator", + "module": "Core", + "name": "Comment", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + }, + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Website Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "comment_type", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py new file mode 100644 index 0000000000..24149276c8 --- /dev/null +++ b/frappe/core/doctype/comment/comment.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals, absolute_import +import frappe +from frappe import _ +import json +from frappe.model.document import Document +from frappe.core.doctype.user.user import extract_mentions +from frappe.utils import get_fullname, get_link_to_form +from frappe.website.render import clear_cache +from frappe.database.schema import add_column +from frappe.exceptions import ImplicitCommitError + +class Comment(Document): + def after_insert(self): + self.notify_mentions() + + frappe.publish_realtime('new_communication', self.as_dict(), + doctype=self.reference_doctype, docname=self.reference_name, + after_commit=True) + + def validate(self): + if not self.comment_email: + self.comment_email = frappe.session.user + + def on_update(self): + update_comment_in_doc(self) + + def on_trash(self): + self.remove_comment_from_cache() + frappe.publish_realtime('delete_communication', self.as_dict(), + doctype= self.reference_doctype, docname = self.reference_name, + after_commit=True) + + def remove_comment_from_cache(self): + _comments = self.get_comments_from_parent() + for c in _comments: + if c.get("name")==self.name: + _comments.remove(c) + + update_comments_in_parent(self.reference_doctype, self.reference_name, _comments) + + def notify_mentions(self): + if self.reference_doctype and self.reference_name and self.content: + mentions = extract_mentions(self.content) + + if not mentions: + return + + sender_fullname = get_fullname(frappe.session.user) + title_field = frappe.get_meta(self.reference_doctype).get_title_field() + title = self.reference_name if title_field == "name" else \ + frappe.db.get_value(self.reference_doctype, self.reference_name, title_field) + + if title != self.reference_name: + parent_doc_label = "{0}: {1} (#{2})".format(_(self.reference_doctype), + title, self.reference_name) + else: + parent_doc_label = "{0}: {1}".format(_(self.reference_doctype), + self.reference_name) + + subject = _("{0} mentioned you in a comment in {1}").format(sender_fullname, parent_doc_label) + + recipients = [frappe.db.get_value("User", {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, "email") + for name in mentions] + link = get_link_to_form(self.reference_doctype, self.reference_name, label=parent_doc_label) + + frappe.sendmail( + recipients = recipients, + sender = frappe.session.user, + subject = subject, + template = "mentioned_in_comment", + args = { + "body_content": _("{0} mentioned you in a comment in {1}").format(sender_fullname, link), + "comment": self, + "link": link + }, + header = [_('New Mention'), 'orange'] + ) + + def get_comments_from_parent(self): + ''' + get the list of comments cached in the document record in the column + `_comments` + ''' + try: + _comments = frappe.db.get_value(self.reference_doctype, self.reference_name, "_comments") or "[]" + + except Exception as e: + if frappe.db.is_missing_table_or_column(e): + _comments = "[]" + + else: + raise + + try: + return json.loads(_comments) + except ValueError: + return [] + +def update_comment_in_doc(doc): + """Updates `_comments` (JSON) property in parent Document. + Creates a column `_comments` if property does not exist. + + Only user created comments Communication or Comment of type Comment are saved. + + `_comments` format + + { + "comment": [String], + "by": [user], + "name": [Comment Document name] + }""" + + # only comments get updates, not likes, assignments etc. + if doc.comment_type != 'Comment': + return + + def get_truncated(content): + return (content[:97] + '...') if len(content) > 100 else content + + if doc.reference_doctype and doc.reference_name and doc.content: + _comments = doc.get_comments_from_parent() + + updated = False + for c in _comments: + if c.get("name")==doc.name: + c["comment"] = get_truncated(doc.content) + updated = True + + if not updated: + _comments.append({ + "comment": get_truncated(doc.content), + "by": doc.comment_email or doc.owner, + "name": doc.name + }) + + update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments) + + +def update_comments_in_parent(reference_doctype, reference_name, _comments): + """Updates `_comments` property in parent Document with given dict. + + :param _comments: Dict of comments.""" + if not reference_doctype or frappe.db.get_value("DocType", reference_doctype, "issingle"): + return + + try: + # use sql, so that we do not mess with the timestamp + frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec + (json.dumps(_comments[-50:]), reference_name)) + + except Exception as e: + if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None): + # missing column and in request, add column and update after commit + frappe.local._comments = (getattr(frappe.local, "_comments", []) + + [(reference_doctype, reference_name, _comments)]) + else: + raise ImplicitCommitError + + else: + if not frappe.flags.in_patch: + reference_doc = frappe.get_doc(reference_doctype, reference_name) + if getattr(reference_doc, "route", None): + clear_cache(reference_doc.route) + +def update_comments_in_parent_after_request(): + """update _comments in parent if _comments column is missing""" + if hasattr(frappe.local, "_comments"): + for (reference_doctype, reference_name, _comments) in frappe.local._comments: + add_column(reference_doctype, "_comments", "Text") + update_comments_in_parent(reference_doctype, reference_name, _comments) + + frappe.db.commit() diff --git a/frappe/core/doctype/comment/test_comment.py b/frappe/core/doctype/comment/test_comment.py new file mode 100644 index 0000000000..0f46f0b3b5 --- /dev/null +++ b/frappe/core/doctype/comment/test_comment.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe, json +import unittest + +class TestComment(unittest.TestCase): + def test_comment_creation(self): + test_doc = frappe.get_doc(dict(doctype = 'ToDo', description = 'test')) + test_doc.insert() + comment = test_doc.add_comment('Comment', 'test comment') + + test_doc.reload() + + # check if updated in _comments cache + comments = json.loads(test_doc.get('_comments')) + self.assertEqual(comments[0].get('name'), comment.name) + self.assertEqual(comments[0].get('comment'), comment.content) + + # check document creation + comment_1 = frappe.get_all('Comment', fields = ['*'], filters = dict( + reference_doctype = test_doc.doctype, + reference_name = test_doc.name + ))[0] + + self.assertEqual(comment_1.content, 'test comment') + + # test via blog + def test_public_comment(self): + from frappe.website.doctype.blog_post.test_blog_post import make_test_blog + test_blog = make_test_blog() + + frappe.db.sql("delete from `tabComment` where reference_doctype = 'Blog Post'") + + from frappe.templates.includes.comments.comments import add_comment + add_comment('hello', 'test@test.com', 'Good Tester', + 'Blog Post', test_blog.name, test_blog.route) + + self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict( + reference_doctype = test_blog.doctype, + reference_name = test_blog.name + ))[0].published, 1) + + frappe.db.sql("delete from `tabComment` where reference_doctype = 'Blog Post'") + + add_comment('pleez vizits my site http://mysite.com', 'test@test.com', 'bad commentor', + 'Blog Post', test_blog.name, test_blog.route) + + self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict( + reference_doctype = test_blog.doctype, + reference_name = test_blog.name + ))[0].published, 0) + + + diff --git a/frappe/core/doctype/communication/comment.py b/frappe/core/doctype/communication/comment.py deleted file mode 100644 index b7a008cd38..0000000000 --- a/frappe/core/doctype/communication/comment.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals, absolute_import -import frappe -from frappe import _ -import json -from frappe.core.doctype.user.user import extract_mentions -from frappe.utils import get_fullname, get_link_to_form -from frappe.website.render import clear_cache -from frappe.database.schema import add_column -from frappe.exceptions import ImplicitCommitError - -def on_trash(doc): - if doc.communication_type != "Comment": - return - - if doc.reference_doctype == "Message": - return - - if (doc.comment_type or "Comment") != "Comment": - frappe.only_for("System Manager") - - _comments = get_comments_from_parent(doc) - for c in _comments: - if c.get("name")==doc.name: - _comments.remove(c) - - update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments) - -def update_comment_in_doc(doc): - """Updates `_comments` (JSON) property in parent Document. - Creates a column `_comments` if property does not exist. - - Only user created comments Communication or Comment of type Comment are saved. - - `_comments` format - - { - "comment": [String], - "by": [user], - "name": [Comment Document name] - }""" - - if doc.communication_type not in ("Comment", "Communication"): - return - - if doc.communication_type == 'Comment' and doc.comment_type != 'Comment': - # other updates - return - - def get_content(doc): - return (doc.content[:97] + '...') if len(doc.content) > 100 else doc.content - - if doc.reference_doctype and doc.reference_name and doc.content: - _comments = get_comments_from_parent(doc) - - updated = False - for c in _comments: - if c.get("name")==doc.name: - c["comment"] = get_content(doc) - updated = True - - if not updated: - _comments.append({ - "comment": get_content(doc), - "by": doc.sender or doc.owner, - "name": doc.name - }) - - update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments) - -def notify_mentions(doc): - if doc.communication_type != "Comment": - return - - if doc.reference_doctype and doc.reference_name and doc.content and doc.comment_type=="Comment": - mentions = extract_mentions(doc.content) - - if not mentions: - return - - sender_fullname = get_fullname(frappe.session.user) - title_field = frappe.get_meta(doc.reference_doctype).get_title_field() - title = doc.reference_name if title_field == "name" else \ - frappe.db.get_value(doc.reference_doctype, doc.reference_name, title_field) - - if title != doc.reference_name: - parent_doc_label = "{0}: {1} (#{2})".format(_(doc.reference_doctype), - title, doc.reference_name) - else: - parent_doc_label = "{0}: {1}".format(_(doc.reference_doctype), - doc.reference_name) - - subject = _("{0} mentioned you in a comment in {1}").format(sender_fullname, parent_doc_label) - - recipients = [frappe.db.get_value("User", {"enabled": 1, "name": name, "user_type": "System User", "allowed_in_mentions": 1}, "email") - for name in mentions] - link = get_link_to_form(doc.reference_doctype, doc.reference_name, label=parent_doc_label) - - frappe.sendmail( - recipients=recipients, - sender=frappe.session.user, - subject=subject, - template="mentioned_in_comment", - args={ - "body_content": _("{0} mentioned you in a comment in {1}").format(sender_fullname, link), - "comment": doc, - "link": link - }, - header=[_('New Mention'), 'orange'] - ) - -def get_comments_from_parent(doc): - try: - _comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]" - - except Exception as e: - if frappe.db.is_missing_table_or_column(e): - _comments = "[]" - - else: - raise - - try: - return json.loads(_comments) - except ValueError: - return [] - -def update_comments_in_parent(reference_doctype, reference_name, _comments): - """Updates `_comments` property in parent Document with given dict. - - :param _comments: Dict of comments.""" - if not reference_doctype or frappe.db.get_value("DocType", reference_doctype, "issingle"): - return - - try: - # use sql, so that we do not mess with the timestamp - frappe.db.sql("""update `tab%s` set `_comments`=%s where name=%s""" % (reference_doctype, - "%s", "%s"), (json.dumps(_comments), reference_name)) - - except Exception as e: - if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None): - # missing column and in request, add column and update after commit - frappe.local._comments = (getattr(frappe.local, "_comments", []) - + [(reference_doctype, reference_name, _comments)]) - else: - raise ImplicitCommitError - - else: - if not frappe.flags.in_patch: - reference_doc = frappe.get_doc(reference_doctype, reference_name) - if getattr(reference_doc, "route", None): - clear_cache(reference_doc.route) - -def add_info_comment(**kwargs): - kwargs.update({ - "doctype": "Communication", - "communication_type": "Comment", - "comment_type": "Info", - "status": "Closed" - }) - return frappe.get_doc(kwargs).insert(ignore_permissions=True) - -def update_comments_in_parent_after_request(): - """update _comments in parent if _comments column is missing""" - if hasattr(frappe.local, "_comments"): - for (reference_doctype, reference_name, _comments) in frappe.local._comments: - add_column(reference_doctype, "_comments", "Text") - update_comments_in_parent(reference_doctype, reference_name, _comments) - - frappe.db.commit() diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index d0a7433e72..3a5d9cf812 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -6,8 +6,6 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import validate_email_add, get_fullname, strip_html, cstr -from frappe.core.doctype.communication.comment import (notify_mentions, - update_comment_in_doc, on_trash) from frappe.core.doctype.communication.email import (validate_email, notify, _notify, update_parent_mins_to_first_response) from frappe.core.utils import get_parent_doc, set_timeline_doc @@ -87,19 +85,15 @@ class Communication(Document): if not (self.reference_doctype and self.reference_name): return - if self.reference_doctype == "Communication" and self.sent_or_received == "Sent" and \ - self.communication_type != 'Comment': + if self.reference_doctype == "Communication" and self.sent_or_received == "Sent": frappe.db.set_value("Communication", self.reference_name, "status", "Replied") - if self.communication_type in ("Communication", "Comment"): + if self.communication_type == "Communication": # send new comment to listening clients frappe.publish_realtime('new_communication', self.as_dict(), doctype=self.reference_doctype, docname=self.reference_name, after_commit=True) - if self.communication_type == "Comment": - notify_mentions(self) - elif self.communication_type in ("Chat", "Notification", "Bot"): if self.reference_name == frappe.session.user: message = self.as_dict() @@ -114,23 +108,14 @@ class Communication(Document): """Update parent status as `Open` or `Replied`.""" if self.comment_type != 'Updated': update_parent_mins_to_first_response(self) - update_comment_in_doc(self) self.bot_reply() def on_trash(self): - if (not self.flags.ignore_permissions - and self.communication_type=="Comment" and self.comment_type != "Comment"): - - # prevent deletion of auto-created comments if not ignore_permissions - frappe.throw(_("Sorry! You cannot delete auto-generated comments")) - - if self.communication_type in ("Communication", "Comment"): + if self.communication_type == "Communication": # send delete comment to listening clients frappe.publish_realtime('delete_communication', self.as_dict(), doctype= self.reference_doctype, docname = self.reference_name, after_commit=True) - # delete the comments from _comment - on_trash(self) def set_status(self): if not self.is_new(): diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index fbcd30a3f2..f29f2ad346 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -33,7 +33,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = :param sender: Communcation sender (default current user). :param recipients: Communication recipients as list. :param communication_medium: Medium of communication (default **Email**). - :param send_mail: Send via email (default **False**). + :param send_email: Send via email (default **False**). :param print_html: HTML Print format to be sent as attachment. :param print_format: Print Format name of parent document to be sent as attachment. :param attachments: List of attachments as list of files or JSON string. @@ -50,6 +50,9 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = if not sender: sender = get_formatted_email(frappe.session.user) + if isinstance(recipients, list): + recipients = ', '.join(recipients) + comm = frappe.get_doc({ "doctype":"Communication", "subject": subject, @@ -307,7 +310,8 @@ def set_incoming_outgoing_accounts(doc): doc.outgoing_email_account = frappe.db.get_value("Email Account", {"append_to": doc.reference_doctype, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender", "name"], as_dict=True) + ["email_id", "always_use_account_email_id_as_sender", "name", + "always_use_account_name_as_sender_name"], as_dict=True) if not doc.incoming_email_account: doc.incoming_email_account = frappe.db.get_value("Email Account", @@ -317,12 +321,14 @@ def set_incoming_outgoing_accounts(doc): # if from address is not the default email account doc.outgoing_email_account = frappe.db.get_value("Email Account", {"email_id": doc.sender, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender", "name", "send_unsubscribe_message"], as_dict=True) or frappe._dict() + ["email_id", "always_use_account_email_id_as_sender", "name", + "send_unsubscribe_message", "always_use_account_name_as_sender_name"], as_dict=True) or frappe._dict() if not doc.outgoing_email_account: doc.outgoing_email_account = frappe.db.get_value("Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender", "name", "send_unsubscribe_message"],as_dict=True) or frappe._dict() + ["email_id", "always_use_account_email_id_as_sender", "name", + "send_unsubscribe_message", "always_use_account_name_as_sender_name"],as_dict=True) or frappe._dict() if doc.sent_or_received == "Sent": doc.db_set("email_account", doc.outgoing_email_account.name) @@ -494,9 +500,9 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments communication._notify(print_html=print_html, print_format=print_format, attachments=attachments, recipients=recipients, cc=cc, bcc=bcc) - except frappe.db.InternalError: + except frappe.db.InternalError as e: # deadlock, try again - if frappe.db.is_deadlocked(): + if frappe.db.is_deadlocked(e): frappe.db.rollback() time.sleep(1) continue diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 3681199bca..f3c23cbfea 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -15,7 +15,7 @@ from frappe import _ from frappe.utils.csvutils import getlink from frappe.utils.dateutils import parse_date -from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_url_to_form +from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url from six import text_type, string_types @@ -418,16 +418,16 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, # log errors if parentfield: log(**{"row": doc.idx, "title": 'Inserted row for "%s"' % (as_link(parenttype, doc.parent)), - "link": get_url_to_form(parenttype, doc.parent), "message": 'Document successfully saved', "indicator": "green"}) + "link": get_absolute_url(parenttype, doc.parent), "message": 'Document successfully saved', "indicator": "green"}) elif submit_after_import: log(**{"row": row_idx + 1, "title":'Submitted row for "%s"' % (as_link(doc.doctype, doc.name)), - "message": "Document successfully submitted", "link": get_url_to_form(doc.doctype, doc.name), "indicator": "blue"}) + "message": "Document successfully submitted", "link": get_absolute_url(doc.doctype, doc.name), "indicator": "blue"}) elif original: log(**{"row": row_idx + 1,"title":'Updated row for "%s"' % (as_link(doc.doctype, doc.name)), - "message": "Document successfully updated", "link": get_url_to_form(doc.doctype, doc.name), "indicator": "green"}) + "message": "Document successfully updated", "link": get_absolute_url(doc.doctype, doc.name), "indicator": "green"}) elif not update_only: log(**{"row": row_idx + 1, "title":'Inserted row for "%s"' % (as_link(doc.doctype, doc.name)), - "message": "Document successfully saved", "link": get_url_to_form(doc.doctype, doc.name), "indicator": "green"}) + "message": "Document successfully saved", "link": get_absolute_url(doc.doctype, doc.name), "indicator": "green"}) else: log(**{"row": row_idx + 1, "title":'Ignored row for %s' % (row[1]), "link": None, "message": "Document updation ignored", "indicator": "orange"}) @@ -444,7 +444,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, error_trace = frappe.get_traceback() if error_trace: error_log_doc = frappe.log_error(error_trace) - error_link = get_url_to_form("Error Log", error_log_doc.name) + error_link = get_absolute_url("Error Log", error_log_doc.name) else: error_link = None diff --git a/frappe/core/doctype/doctype/boilerplate/controller._py b/frappe/core/doctype/doctype/boilerplate/controller._py index d6af4f15aa..65f9b2bc35 100644 --- a/frappe/core/doctype/doctype/boilerplate/controller._py +++ b/frappe/core/doctype/doctype/boilerplate/controller._py @@ -3,7 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe +# import frappe from frappe.model.document import Document class {classname}(Document): diff --git a/frappe/core/doctype/doctype/boilerplate/controller.js b/frappe/core/doctype/doctype/boilerplate/controller.js index 87c69d29ad..6d9fb2a514 100644 --- a/frappe/core/doctype/doctype/boilerplate/controller.js +++ b/frappe/core/doctype/doctype/boilerplate/controller.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('{doctype}', {{ - refresh: function(frm) {{ + // refresh: function(frm) {{ - }} + // }} }}); diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index ce252c2d70..4edf29d937 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -52,7 +52,6 @@ class DocType(Document): self.permissions = [] self.scrub_field_names() - self.scrub_options_in_select() self.set_default_in_list_view() self.set_default_translatable() self.validate_series() @@ -202,17 +201,6 @@ class DocType(Document): # unique is automatically an index if d.unique: d.search_index = 0 - def scrub_options_in_select(self): - """Strip options for whitespaces""" - for field in self.fields: - if field.fieldtype == "Select" and field.options is not None: - options_list = [] - for i, option in enumerate(field.options.split("\n")): - _option = option.strip() - if i==0 or _option: - options_list.append(_option) - field.options = '\n'.join(options_list) - def validate_series(self, autoname=None, name=None): """Validate if `autoname` property is correctly set.""" if not autoname: autoname = self.autoname @@ -705,6 +693,20 @@ def validate_fields(meta): frappe.throw(_('DocType {0} provided for the field {1} must have atleast one Link field') .format(doctype, docfield.fieldname), frappe.ValidationError) + def scrub_options_in_select(field): + """Strip options for whitespaces""" + + if field.fieldtype == "Select" and field.options is not None: + options_list = [] + for i, option in enumerate(field.options.split("\n")): + _option = option.strip() + if i==0 or _option: + options_list.append(_option) + field.options = '\n'.join(options_list) + + def scrub_fetch_from(field): + if hasattr(field, 'fetch_from') and getattr(field, 'fetch_from'): + field.fetch_from = field.fetch_from.strip('\n').strip() fields = meta.get("fields") fieldname_list = [d.fieldname for d in fields] @@ -734,6 +736,8 @@ def validate_fields(meta): check_unique_and_text(d) check_illegal_depends_on_conditions(d) check_table_multiselect_option(d) + scrub_options_in_select(d) + scrub_fetch_from(d) check_fold(fields) check_search_fields(meta, fields) diff --git a/frappe/core/doctype/domain/domain.py b/frappe/core/doctype/domain/domain.py index ea21e73b95..a4e9f503ab 100644 --- a/frappe/core/doctype/domain/domain.py +++ b/frappe/core/doctype/domain/domain.py @@ -18,8 +18,7 @@ class Domain(Document): self.setup_roles() self.setup_properties() self.set_values() - # always set the desktop icons while changing the domain settings - self.setup_desktop_icons() + if not int(frappe.defaults.get_defaults().setup_complete or 0): # if setup not complete, setup desktop etc. self.setup_sidebar_items() @@ -89,12 +88,6 @@ class Domain(Document): frappe.db.set_value('Portal Settings', None, 'default_role', self.data.get('default_portal_role')) - def setup_desktop_icons(self): - '''set desktop icons form `data.desktop_icons`''' - from frappe.desk.doctype.desktop_icon.desktop_icon import set_desktop_icons - if self.data.desktop_icons: - set_desktop_icons(self.data.desktop_icons) - def setup_properties(self): if self.data.properties: for args in self.data.properties: diff --git a/frappe/core/doctype/language/language.py b/frappe/core/doctype/language/language.py index 8c7e01cb62..fb18abdf5e 100644 --- a/frappe/core/doctype/language/language.py +++ b/frappe/core/doctype/language/language.py @@ -3,11 +3,22 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe, json +import frappe, json, re +from frappe import _ from frappe.model.document import Document class Language(Document): - pass + def validate(self): + validate_with_regex(self.language_code, "Language Code") + + def before_rename(self, old, new, merge=False): + validate_with_regex(new, "Name") + +def validate_with_regex(name, label): + pattern = re.compile("^[a-zA-Z]+[-_]*[a-zA-Z]+$") + if not pattern.match(name): + frappe.throw(_("""{0} must begin and end with a letter and can only contain letters, + hyphen or underscore.""").format(label)) def export_languages_json(): '''Export list of all languages''' diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 87c14c6651..6b40cd7d33 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -152,6 +152,8 @@ class Report(Document): if params.get('sort_by'): order_by = _format(params.get('sort_by').split('.')) + ' ' + params.get('sort_order') + elif params.get('order_by'): + order_by = params.get('order_by') else: order_by = _format([self.ref_doctype, 'modified']) + ' desc' diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 71d7cfc0e8..d334731015 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -71,10 +71,6 @@ frappe.ui.form.on('User', { frm.toggle_display(['sb1', 'sb3', 'modules_access'], false); if(!frm.is_new()) { - frm.add_custom_button(__("Set Desktop Icons"), function() { - frappe.frappe_toolbar.modules_select.show(doc.name); - }, null, "btn-default") - if(has_access_to_edit_user()) { frm.add_custom_button(__("Set User Permissions"), function() { diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 4fcee60dc5..6d429f3248 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1511,7 +1511,7 @@ "columns": 0, "default": "", "description": "", - "fieldname": "desktop_icon_access", + "fieldname": "sb_allow_modules", "fieldtype": "Section Break", "hidden": 0, "ignore_user_permissions": 0, @@ -1520,7 +1520,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Allow Desktop Icon", + "label": "Allow Modules", "length": 0, "no_copy": 0, "permlevel": 1, @@ -2303,7 +2303,7 @@ "issingle": 0, "istable": 0, "max_attachments": 5, - "modified": "2018-11-21 12:34:57.652854", + "modified": "2019-01-30 13:56:10.732154", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index b191496c73..087d7c9d21 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -36,9 +36,9 @@ class User(Document): self.name = self.email def onload(self): + from frappe.config import get_modules_from_all_apps self.set_onload('all_modules', - [m.module_name for m in frappe.db.get_all('Desktop Icon', - fields=['module_name'], filters={'standard': 1}, order_by="module_name")]) + [m.get("module_name") for m in get_modules_from_all_apps()]) def before_insert(self): self.flags.in_insert = True @@ -358,6 +358,9 @@ class User(Document): WHERE `%s` = %s""" % (tab, field, '%s', field, '%s'), (new_name, old_name)) + if frappe.db.exists("Chat Profile", old_name): + frappe.rename_doc("Chat Profile", old_name, new_name, force=True) + # set email frappe.db.sql("""UPDATE `tabUser` SET email = %s diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py index 157fa44ae2..b83d103013 100644 --- a/frappe/core/doctype/user_permission/test_user_permission.py +++ b/frappe/core/doctype/user_permission/test_user_permission.py @@ -2,9 +2,84 @@ # Copyright (c) 2017, Frappe Technologies and Contributors # See license.txt from __future__ import unicode_literals +from frappe.core.doctype.user_permission.user_permission import add_user_permissions -#import frappe +import frappe import unittest class TestUserPermission(unittest.TestCase): - pass + def test_apply_to_all(self): + ''' Create User permission for User having access to all applicable Doctypes''' + user = get_user() + param = get_params(user, apply = 1) + created = add_user_permissions(param) + self.assertEquals(created, 1) + + def test_for_applicable_on_update_from_apply_to_all(self): + ''' Update User Permission from all to some applicable Doctypes''' + user = get_user() + param = get_params(user, applicable = ["Chat Room", "Chat Message"]) + create = add_user_permissions(param) + frappe.db.commit() + + removed_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user)) + created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room")) + created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message")) + + self.assertIsNone(removed_apply_to_all) + self.assertIsNotNone(created_applicable_first) + self.assertIsNotNone(created_applicable_second) + self.assertEquals(create, 1) + + def test_for_apply_to_all_on_update_from_applicable(self): + ''' Update User Permission from some to all applicable Doctypes''' + user = get_user() + param = get_params(user, apply = 1) + created = add_user_permissions(param) + created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user)) + removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room")) + removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message")) + + + self.assertIsNotNone(created_apply_to_all) + self.assertIsNone(removed_applicable_first) + self.assertIsNone(removed_applicable_second) + self.assertEquals(created, 1) + +def get_user(): + if frappe.db.exists('User', 'test_bulk_creation_update@example.com'): + return frappe.get_doc('User', 'test_bulk_creation_update@example.com') + else: + user = frappe.new_doc('User') + user.email = 'test_bulk_creation_update@example.com' + user.first_name = 'Test_Bulk_Creation' + user.add_roles("System Manager") + return user + +def get_params(user, apply = None , applicable = None): + ''' Return param to insert ''' + param = { + "user": user.name, + "doctype":"User", + "docname":user.name + } + if apply: + param.update({"apply_to_all_doctypes": 1}) + param.update({"applicable_doctypes": []}) + if applicable: + param.update({"apply_to_all_doctypes": 0}) + param.update({"applicable_doctypes": applicable}) + return param + +def get_exists_param(user, applicable = None): + ''' param to check existing Document ''' + param = { + "user": user.name, + "allow": "User", + "for_value": user.name, + } + if applicable: + param.update({"applicable_for": applicable}) + else: + param.update({"apply_to_all_doctypes": 1}) + return param diff --git a/frappe/core/doctype/user_permission/user_permission.json b/frappe/core/doctype/user_permission/user_permission.json index 9a38f8d953..c2ea05e731 100644 --- a/frappe/core/doctype/user_permission/user_permission.json +++ b/frappe/core/doctype/user_permission/user_permission.json @@ -41,7 +41,7 @@ "remember_last_selected_value": 0, "report_hide": 0, "reqd": 1, - "search_index": 0, + "search_index": 1, "set_only_once": 0, "translatable": 0, "unique": 0 @@ -222,7 +222,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-11-12 16:26:12.362352", + "modified": "2019-02-13 22:58:27.428741", "modified_by": "Administrator", "module": "Core", "name": "User Permission", diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index f8099cac38..cdd008b1e4 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -42,6 +42,9 @@ def get_user_permissions(user=None): if not user: user = frappe.session.user + if user == "Administrator": + return {} + cached_user_permissions = frappe.cache().hget("user_permissions", user) if cached_user_permissions is not None: @@ -111,12 +114,100 @@ def get_permitted_documents(doctype): return [d.get('doc') for d in get_user_permissions().get(doctype, []) \ if d.get('doc')] +@frappe.whitelist() +def check_applicable_doc_perm(user, doctype, docname): + frappe.only_for('System Manager') + applicable = [] + doc_exists = frappe.get_all('User Permission', + fields=['name'], + filters={"user": user, + "allow": doctype, + "for_value": docname, + "apply_to_all_doctypes":1, + }, limit=1) + if doc_exists: + applicable = get_linked_doctypes(doctype).keys() + else: + data = frappe.get_all('User Permission', + fields=['applicable_for'], + filters={"user": user, + "allow": doctype, + "for_value":docname, + }) + for d in data: + applicable.append(d.applicable_for) + return applicable + + @frappe.whitelist() def clear_user_permissions(user, for_doctype): frappe.only_for('System Manager') - total = frappe.db.count('User Permission', filters = dict(user=user, allow=for_doctype)) if total: frappe.db.sql('DELETE FROM `tabUser Permission` WHERE `user`=%s AND `allow`=%s', (user, for_doctype)) frappe.clear_cache() return total + +@frappe.whitelist() +def add_user_permissions(data): + ''' Add and update the user permissions ''' + frappe.only_for('System Manager') + if isinstance(data, frappe.string_types): + data = json.loads(data) + data = frappe._dict(data) + + d = check_applicable_doc_perm(data.user, data.doctype, data.docname) + exists = frappe.db.exists("User Permission", {"user": data.user, "allow": data.doctype, "for_value": data.docname, "apply_to_all_doctypes": 1}) + if data.apply_to_all_doctypes == 1 and not exists: + remove_applicable(d, data.user, data.doctype, data.docname) + insert_user_perm(data.user, data.doctype, data.docname, apply_to_all = 1) + return 1 + else: + remove_apply_to_all(data.user, data.doctype, data.docname) + update_applicable(d, data.applicable_doctypes, data.user, data.doctype, data.docname) + for applicable in data.applicable_doctypes : + if applicable not in d: + insert_user_perm(data.user, data.doctype, data.docname, applicable = applicable) + elif exists: + insert_user_perm(data.user, data.doctype, data.docname, applicable = applicable) + return 1 + return 0 + +def insert_user_perm(user, doctype, docname, apply_to_all=None, applicable=None): + user_perm = frappe.new_doc("User Permission") + user_perm.user = user + user_perm.allow = doctype + user_perm.for_value = docname + if applicable: + user_perm.applicable_for = applicable + user_perm.apply_to_all_doctypes = 0 + else: + user_perm.apply_to_all_doctypes = 1 + user_perm.insert() + +def remove_applicable(d, user, doctype, docname): + for applicable_for in d: + frappe.db.sql("""DELETE FROM `tabUser Permission` + WHERE `user`=%s + AND `applicable_for`=%s + AND `allow`=%s + AND `for_value`=%s + """, (user, applicable_for, doctype, docname)) + +def remove_apply_to_all(user, doctype, docname): + frappe.db.sql("""DELETE from `tabUser Permission` + WHERE `user`=%s + AND `apply_to_all_doctypes`=1 + AND `allow`=%s + AND `for_value`=%s + """,(user, doctype, docname)) + +def update_applicable(already_applied, to_apply, user, doctype, docname): + for applied in already_applied: + if applied not in to_apply: + frappe.db.sql("""DELETE FROM `tabUser Permission` + WHERE `user`=%s + AND `applicable_for`=%s + AND `allow`=%s + AND `for_value`=%s + """,(user, applied, doctype, docname)) \ No newline at end of file diff --git a/frappe/core/doctype/user_permission/user_permission_list.js b/frappe/core/doctype/user_permission/user_permission_list.js index 39a4648334..00d829b2a0 100644 --- a/frappe/core/doctype/user_permission/user_permission_list.js +++ b/frappe/core/doctype/user_permission/user_permission_list.js @@ -1,22 +1,123 @@ frappe.listview_settings['User Permission'] = { + onload: function(list_view) { - list_view.page.add_menu_item(__("Clear User Permissions"), () => { + var me = this; + list_view.page.add_inner_button( __("Add / Update"), function() { + let dialog =new frappe.ui.Dialog({ + title : __('Add User Permissions'), + fields: [ + { + fieldname: 'user', + label: __('For User'), + fieldtype: 'Link', + options: 'User', + reqd: 1, + onchange: function() { + dialog.fields_dict.doctype.set_input(undefined); + dialog.fields_dict.docname.set_input(undefined); + dialog.set_df_property("docname", "hidden", 1); + dialog.set_df_property("apply_to_all_doctypes", "hidden", 1); + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } + }, + { + fieldname: 'doctype', + label: __('Document Type'), + fieldtype: 'Link', + options: 'DocType', + reqd: 1, + onchange: function() { + me.on_doctype_change(dialog); + } + }, + { + fieldname: 'docname', + label: __('Document Name'), + fieldtype: 'Dynamic Link', + options: 'doctype', + hidden: 1, + onchange: function() { + let field = dialog.fields_dict["docname"]; + if(field.value != field.last_value) { + if(dialog.fields_dict.doctype.value && dialog.fields_dict.docname.value && dialog.fields_dict.user.value){ + me.get_applicable_doctype(dialog).then(applicable => { + me.get_multi_select_options(dialog, applicable).then(options => { + me.applicable_options = options; + me.on_docname_change(dialog, options, applicable); + if(options.length > 5){ + dialog.fields_dict.applicable_doctypes.setup_select_all(); + } + }); + }); + } + } + } + }, + { + fieldname: 'apply_to_all_doctypes', + label: __('Apply to all Documents Types'), + fieldtype: 'Check', + checked: 1, + hidden: 1, + onchange: function() { + if(dialog.fields_dict.doctype.value && dialog.fields_dict.docname.value && dialog.fields_dict.user.value){ + me.on_apply_to_all_doctypes_change(dialog, me.applicable_options); + if(me.applicable_options.length > 5){ + dialog.fields_dict.applicable_doctypes.setup_select_all(); + } + } + } + }, + { + label: __("Applicable Document Types"), + fieldname: "applicable_doctypes", + fieldtype: "MultiCheck", + options: [], + columns: 2, + hidden: 1 + }, + ], + primary_action: (data) => { + data = me.validate(dialog, data); + frappe.call({ + async: false, + method: "frappe.core.doctype.user_permission.user_permission.add_user_permissions", + args: { + data : data + }, + callback: function(r) { + if(r.message === 1) { + frappe.show_alert({message:__("User Permissions created sucessfully"), indicator:'blue'}); + } else { + frappe.show_alert({message:__("Nothing to update"), indicator:'red'}); + + } + } + }); + dialog.hide(); + list_view.refresh(); + }, + primary_action_label: __('Submit') + }); + dialog.show(); + }); + list_view.page.add_inner_button( __("Bulk Delete"), function() { const dialog = new frappe.ui.Dialog({ title: __('Clear User Permissions'), fields: [ { - 'fieldname': 'user', - 'label': __('For User'), - 'fieldtype': 'Link', - 'options': 'User', - 'reqd': 1 + fieldname: 'user', + label: __('For User'), + fieldtype: 'Link', + options: 'User', + reqd: 1 }, { - 'fieldname': 'for_doctype', - 'label': __('For Document Type'), - 'fieldtype': 'Link', - 'options': 'DocType', - 'reqd': 1 + fieldname: 'for_doctype', + label: __('For Document Type'), + fieldtype: 'Link', + options: 'DocType', + reqd: 1 }, ], primary_action: (data) => { @@ -31,6 +132,8 @@ frappe.listview_settings['User Permission'] = { let message = ''; if (data === 0) { message = __('No records deleted'); + } else if(data === 1) { + message = __('{0} record deleted', [data]); } else { message = __('{0} records deleted', [data]); } @@ -43,10 +146,95 @@ frappe.listview_settings['User Permission'] = { }); }, - primary_action_label: __('Clear') + primary_action_label: __('Delete') }); dialog.show(); }); + }, + + validate: function(dialog, data) { + if(dialog.fields_dict.applicable_doctypes.get_unchecked_options().length == 0) { + data.apply_to_all_doctypes = 1; + data.applicable_doctypes = []; + return data; + } + if(data.apply_to_all_doctypes == 0 && !("applicable_doctypes" in data)) { + frappe.throw("Please select applicable Doctypes"); + } + return data; + }, + + get_applicable_doctype: function(dialog) { + return new Promise(resolve => { + frappe.call({ + method: 'frappe.core.doctype.user_permission.user_permission.check_applicable_doc_perm', + async: false, + args:{ + user: dialog.fields_dict.user.value, + doctype: dialog.fields_dict.doctype.value, + docname: dialog.fields_dict.docname.value + } + }).then(r => { + resolve(r.message); + }); + }); + }, + + get_multi_select_options: function(dialog, applicable){ + return new Promise(resolve => { + frappe.call({ + method: 'frappe.desk.form.linked_with.get_linked_doctypes', + async: false, + args:{ + user: dialog.fields_dict.user.value, + doctype: dialog.fields_dict.doctype.value, + docname: dialog.fields_dict.docname.value + } + }).then(r => { + var options = []; + for(var d in r.message){ + var checked = ($.inArray(d, applicable) != -1) ? 1 : 0; + options.push({ "label":d, "value": d , "checked": checked}); + } + resolve(options); + }); + }); + }, + + on_doctype_change: function(dialog) { + dialog.set_df_property("docname", "hidden", 0); + dialog.set_df_property("docname", "reqd", 1); + dialog.set_df_property("apply_to_all_doctypes", "hidden", 0); + dialog.set_value("apply_to_all_doctypes","checked",1); + }, + + on_docname_change: function(dialog, options, applicable) { + if(applicable.length != 0 ) { + dialog.set_primary_action("Update"); + dialog.set_title("Update User Permissions"); + dialog.set_df_property("applicable_doctypes", "options", options); + if(dialog.fields_dict.applicable_doctypes.get_checked_options().length == options.length) { + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } else { + dialog.set_df_property("applicable_doctypes", "hidden", 0); + dialog.set_df_property("apply_to_all_doctypes", "checked", 0); + } + } else { + dialog.set_primary_action("Submit"); + dialog.set_title("Add User Permissions"); + dialog.set_df_property("applicable_doctypes", "options", options); + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } + }, + + on_apply_to_all_doctypes_change: function(dialog, options) { + if(dialog.fields_dict.apply_to_all_doctypes.get_value() == 0) { + dialog.set_df_property("applicable_doctypes", "hidden", 0); + dialog.set_df_property("applicable_doctypes", "options", options); + } else { + dialog.set_df_property("applicable_doctypes", "options", options); + dialog.set_df_property("applicable_doctypes", "hidden", 1); + } } -}; +}; \ No newline at end of file diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py index 619231e5cd..0f932d9f67 100644 --- a/frappe/core/notifications.py +++ b/frappe/core/notifications.py @@ -44,14 +44,14 @@ def get_todays_events(as_list=False): def get_unseen_likes(): """Returns count of unseen likes""" - return frappe.db.sql("""select count(*) from `tabCommunication` + return frappe.db.sql("""select count(*) from `tabComment` where - communication_type='Comment' + comment_type='Like' and modified >= (NOW() - INTERVAL '1' YEAR) - and comment_type='Like' and owner is not null and owner!=%(user)s and reference_owner=%(user)s - and seen=0""", {"user": frappe.session.user})[0][0] + and seen=0 + """, {"user": frappe.session.user})[0][0] def get_unread_emails(): "returns unread emails for a user" diff --git a/frappe/core/page/desktop/README.md b/frappe/core/page/desktop/README.md deleted file mode 100644 index 4ac65f50b5..0000000000 --- a/frappe/core/page/desktop/README.md +++ /dev/null @@ -1 +0,0 @@ -First screen after login. Array of module icons based on permission. \ No newline at end of file diff --git a/frappe/core/page/desktop/__init__.py b/frappe/core/page/desktop/__init__.py index 0e57cb68c3..e69de29bb2 100644 --- a/frappe/core/page/desktop/__init__.py +++ b/frappe/core/page/desktop/__init__.py @@ -1,3 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - diff --git a/frappe/core/page/desktop/all_applications_dialog.html b/frappe/core/page/desktop/all_applications_dialog.html deleted file mode 100644 index d26152c84b..0000000000 --- a/frappe/core/page/desktop/all_applications_dialog.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
-{% if (frappe.user.has_role("System Manager")) { %} -

Install new applications -

-{% } %} -
-

{%= __("Checked items will be shown on desktop") %}

-
- {% for(var i=0, l=all_modules.length; i < l; i++) { - var module_name = all_modules[i]; - var module = frappe.get_module(module_name); - if (desktop_items.indexOf(module_name)===-1 - || frappe.user.is_module_blocked(module_name)) { continue; } - %} -
-
- -
-
- {% } %} -
diff --git a/frappe/core/page/desktop/desktop.js b/frappe/core/page/desktop/desktop.js index 24dd0c3b4e..13b64c9246 100644 --- a/frappe/core/page/desktop/desktop.js +++ b/frappe/core/page/desktop/desktop.js @@ -1,344 +1 @@ -frappe.provide('frappe.desktop'); - -frappe.pages['desktop'].on_page_load = function(wrapper) { - - // load desktop - if(!frappe.list_desktop) { - frappe.desktop.set_background(); - } - frappe.desktop.refresh(wrapper); -}; - -frappe.pages['desktop'].on_page_show = function(wrapper) { - if(frappe.list_desktop) { - $("body").attr("data-route", "list-desktop"); - } -}; - -$.extend(frappe.desktop, { - refresh: function(wrapper) { - if (wrapper) { - this.wrapper = $(wrapper); - } - - this.render(); - this.make_sortable(); - }, - - render: function() { - var me = this; - frappe.utils.set_title(__("Desktop")); - - var template = frappe.list_desktop ? "desktop_list_view" : "desktop_icon_grid"; - - var all_icons = frappe.get_desktop_icons(); - var explore_icon = { - module_name: 'Explore', - label: 'Explore', - _label: __('Explore'), - _id: 'Explore', - _doctype: '', - icon: 'octicon octicon-telescope', - color: '#7578f6', - link: 'modules' - }; - explore_icon.app_icon = frappe.ui.app_icon.get_html(explore_icon); - all_icons.push(explore_icon); - - frappe.desktop.wrapper.html(frappe.render_template(template, { - // all visible icons - desktop_items: all_icons, - })); - - frappe.desktop.setup_module_click(); - - // notifications - frappe.desktop.show_pending_notifications(); - $(document).on("notification-update", function() { - me.show_pending_notifications(); - }); - - $(document).trigger("desktop-render"); - - }, - - render_help_messages: function(help_messages) { - var wrapper = frappe.desktop.wrapper.find('.help-message-wrapper'); - var $help_messages = wrapper.find('.help-messages'); - - var set_current_message = function(idx) { - idx = cint(idx); - wrapper.current_message_idx = idx; - wrapper.find('.left-arrow, .right-arrow').addClass('disabled'); - wrapper.find('.help-message-item').addClass('hidden'); - wrapper.find('[data-message-idx="'+idx+'"]').removeClass('hidden'); - if(idx > 0) { - wrapper.find('.left-arrow').removeClass('disabled'); - } - if(idx < help_messages.length - 1) { - wrapper.find('.right-arrow').removeClass('disabled'); - } - } - - if(help_messages) { - wrapper.removeClass('hidden'); - help_messages.forEach(function(message, i) { - var $message = $('') - .attr('data-message-idx', i) - .html(frappe.render_template('desktop_help_message', message)) - .appendTo($help_messages); - - }); - - set_current_message(0); - - wrapper.find('.close').on('click', function() { - wrapper.addClass('hidden'); - }); - } - - wrapper.find('.left-arrow').on('click', function() { - if(wrapper.current_message_idx) { - set_current_message(wrapper.current_message_idx - 1); - } - }) - - wrapper.find('.right-arrow').on('click', function() { - if(help_messages.length > wrapper.current_message_idx + 1) { - set_current_message(wrapper.current_message_idx + 1); - } - }); - - }, - - setup_module_click: function() { - frappe.desktop.wiggling = false; - - if(frappe.list_desktop) { - frappe.desktop.wrapper.on("click", ".desktop-list-item", function() { - frappe.desktop.open_module($(this)); - }); - } else { - frappe.desktop.wrapper.on("click", ".app-icon, .app-icon-svg", function() { - if ( !frappe.desktop.wiggling ) { - frappe.desktop.open_module($(this).parent()); - } - }); - } - frappe.desktop.wrapper.on("click", ".circle", function() { - var doctype = $(this).attr('data-doctype'); - if(doctype) { - frappe.ui.notifications.show_open_count_list(doctype); - } - }); - - frappe.desktop.setup_wiggle(); - }, - - setup_wiggle: () => { - // Wiggle, Wiggle, Wiggle. - const DURATION_LONG_PRESS = 1000; - - var timer_id = 0; - const $cases = frappe.desktop.wrapper.find('.case-wrapper'); - const $icons = frappe.desktop.wrapper.find('.app-icon'); - const $notis = $(frappe.desktop.wrapper.find('.circle').toArray().filter((object) => { - // This hack is so bad, I should punch myself. - // Seriously, punch yourself. - const text = $(object).find('.circle-text').html(); - - return text; - })); - - const clearWiggle = () => { - const $closes = $cases.find('.module-remove'); - $closes.hide(); - $notis.show(); - - $icons.removeClass('wiggle'); - - frappe.desktop.wiggling = false; - }; - - frappe.desktop.wrapper.on('mousedown', '.app-icon', () => { - timer_id = setTimeout(() => { - frappe.desktop.wiggling = true; - // hide all notifications. - $notis.hide(); - - $cases.each((i) => { - const $case = $($cases[i]); - const template = - ` -
-
- - × - -
-
- `; - - $case.append(template); - const $close = $case.find('.module-remove'); - const name = $case.attr('title'); - $close.click(() => { - // good enough to create dynamic dialogs? - const dialog = new frappe.ui.Dialog({ - title: __(`Hide ${name}?`) - }); - dialog.set_primary_action(__('Hide'), () => { - frappe.call({ - method: 'frappe.desk.doctype.desktop_icon.desktop_icon.hide', - args: { name: name }, - freeze: true, - callback: (response) => - { - if ( response.message ) { - location.reload(); - } - } - }) - - dialog.hide(); - - clearWiggle(); - }); - // Hacks, Hacks and Hacks. - var $cancel = dialog.get_close_btn(); - $cancel.click(() => { - clearWiggle(); - }); - $cancel.html(__(`Cancel`)); - - dialog.show(); - }); - }); - - $icons.addClass('wiggle'); - - }, DURATION_LONG_PRESS); - }); - frappe.desktop.wrapper.on('mouseup mouseleave', '.app-icon', () => { - clearTimeout(timer_id); - }); - - // also stop wiggling if clicked elsewhere. - $('body').click((event) => { - if ( frappe.desktop.wiggling ) { - const $target = $(event.target); - // our target shouldn't be .app-icons or .close - const $parent = $target.parents('.case-wrapper'); - if ( $parent.length == 0 ) - clearWiggle(); - } - }); - // end wiggle - }, - - open_module: function(parent) { - var link = parent.attr("data-link"); - if(link) { - if(link.indexOf('javascript:')===0) { - eval(link.substr(11)); - } else if(link.substr(0, 1)==="/" || link.substr(0, 4)==="http") { - window.open(link, "_blank"); - } else { - frappe.set_route(link); - } - return false; - } else { - var module = frappe.get_module(parent.attr("data-name")); - if (module && module.onclick) { - module.onclick(); - return false; - } - } - }, - - make_sortable: function() { - if (frappe.dom.is_touchscreen() || frappe.list_desktop) { - return; - } - - new Sortable($("#icon-grid").get(0), { - animation: 150, - onUpdate: function(event) { - var new_order = []; - $("#icon-grid .case-wrapper").each(function(i, e) { - new_order.push($(this).attr("data-name")); - }); - - frappe.call({ - method: 'frappe.desk.doctype.desktop_icon.desktop_icon.set_order', - args: { - 'new_order': new_order, - 'user': frappe.session.user - }, - quiet: true - }); - } - }); - }, - - set_background: function() { - frappe.ui.set_user_background(frappe.boot.user.background_image, null, - frappe.boot.user.background_style); - }, - - show_pending_notifications: function() { - var modules_list = frappe.get_desktop_icons(); - for (var i=0, l=modules_list.length; i < l; i++) { - var module = modules_list[i]; - - var module_doctypes = frappe.boot.notification_info.module_doctypes[module.module_name]; - - var sum = 0; - - if(module_doctypes && frappe.boot.notification_info.open_count_doctype) { - // sum all doctypes for a module - for (var j=0, k=module_doctypes.length; j < k; j++) { - var doctype = module_doctypes[j]; - let count = (frappe.boot.notification_info.open_count_doctype[doctype] || 0); - count = typeof count == "string" ? parseInt(count) : count; - sum += count; - } - } - - if(frappe.boot.notification_info.open_count_doctype - && frappe.boot.notification_info.open_count_doctype[module.module_name]!=null) { - // notification count explicitly for doctype - let count = frappe.boot.notification_info.open_count_doctype[module.module_name] || 0; - count = typeof count == "string" ? parseInt(count) : count; - sum += count; - } - - if(frappe.boot.notification_info.open_count_module - && frappe.boot.notification_info.open_count_module[module.module_name]!=null) { - // notification count explicitly for module - let count = frappe.boot.notification_info.open_count_module[module.module_name] || 0; - count = typeof count == "string" ? parseInt(count) : count; - sum += count; - } - - // if module found - if(module._id.indexOf('/')===-1 && !module._report) { - var notifier = $(".module-count-" + frappe.scrub(module._id)); - if(notifier.length) { - notifier.toggle(sum ? true : false); - var circle = notifier.find(".circle-text"); - var text = sum || ''; - if(text > 99) { - text = '99+'; - } - - if(circle.length) { - circle.html(text); - } else { - notifier.html(text); - } - } - } - } - } -}); +frappe.pages['desktop'].on_page_load = function() {}; \ No newline at end of file diff --git a/frappe/core/page/desktop/desktop.json b/frappe/core/page/desktop/desktop.json index 6255e3e6fe..66bbfbfd40 100644 --- a/frappe/core/page/desktop/desktop.json +++ b/frappe/core/page/desktop/desktop.json @@ -1,10 +1,11 @@ { - "creation": "2013-02-14 17:37:37.000000", + "content": null, + "creation": "2019-01-29 13:11:48.872579", "docstatus": 0, "doctype": "Page", - "icon": "fa fa-th", - "idx": 1, - "modified": "2013-07-11 14:41:56.000000", + "icon": "icon-th", + "idx": 0, + "modified": "2019-01-29 13:11:48.872579", "modified_by": "Administrator", "module": "Core", "name": "desktop", @@ -15,6 +16,9 @@ "role": "All" } ], + "script": null, "standard": "Yes", + "style": null, + "system_page": 0, "title": "Desktop" } \ No newline at end of file diff --git a/frappe/core/page/desktop/desktop.py b/frappe/core/page/desktop/desktop.py deleted file mode 100644 index f426a67979..0000000000 --- a/frappe/core/page/desktop/desktop.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import unicode_literals - -import functools -import frappe -from past.builtins import cmp - -@frappe.whitelist() -def get_help_messages(): - '''Return help messages for the desktop (called via `get_help_messages` hook) - - Format for message: - - { - title: _('Add Employees to Manage Them'), - description: _('Add your Employees so you can manage leaves, expenses and payroll'), - action: 'Add Employee', - route: 'List/Employee' - } - - ''' - messages = [] - for fn in frappe.get_hooks('get_help_messages'): - messages += frappe.get_attr(fn)() - - return sorted(messages, key = functools.cmp_to_key(lambda a, b: cmp(a.get('count'), b.get('count')))) diff --git a/frappe/core/page/desktop/desktop_help_message.html b/frappe/core/page/desktop/desktop_help_message.html deleted file mode 100644 index 7de47abf03..0000000000 --- a/frappe/core/page/desktop/desktop_help_message.html +++ /dev/null @@ -1,8 +0,0 @@ -
{{ title }}
-

{{ description }}

-
- {{ action }} - - - -
\ No newline at end of file diff --git a/frappe/core/page/desktop/desktop_icon_grid.html b/frappe/core/page/desktop/desktop_icon_grid.html deleted file mode 100644 index 6b85c8a56e..0000000000 --- a/frappe/core/page/desktop/desktop_icon_grid.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
- {% for (var i=0, l=desktop_items.length; i < l; i++) { %} - {{ frappe.render_template("desktop_module_icon", desktop_items[i]) }} - {% } %} -
- -
-
diff --git a/frappe/core/page/desktop/desktop_list_view.html b/frappe/core/page/desktop/desktop_list_view.html deleted file mode 100644 index d3dd514076..0000000000 --- a/frappe/core/page/desktop/desktop_list_view.html +++ /dev/null @@ -1,23 +0,0 @@ -
-
-
-
- {% for (var i=0, l=desktop_items.length; i < l; i++) { - var module = desktop_items[i]; - %} -
-

- - {{ module._label }} -

- -
- {% } %} -
-
-
-
diff --git a/frappe/core/page/desktop/desktop_module_icon.html b/frappe/core/page/desktop/desktop_module_icon.html deleted file mode 100644 index 3e9a451eec..0000000000 --- a/frappe/core/page/desktop/desktop_module_icon.html +++ /dev/null @@ -1,11 +0,0 @@ -
- {{ app_icon }} -
- - - {{ _label }} -
-
diff --git a/frappe/core/page/recorder/__init__.py b/frappe/core/page/recorder/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/page/recorder/recorder.js b/frappe/core/page/recorder/recorder.js new file mode 100644 index 0000000000..f38af41af0 --- /dev/null +++ b/frappe/core/page/recorder/recorder.js @@ -0,0 +1,26 @@ +frappe.pages['recorder'].on_page_load = function(wrapper) { + frappe.ui.make_app_page({ + parent: wrapper, + title: 'Recorder', + single_column: true + }); + + frappe.recorder = new Recorder(wrapper); + $(wrapper).bind('show', function() { + frappe.recorder.show(); + }); + + frappe.require('/assets/js/frappe-recorder.min.js'); +}; + +class Recorder { + constructor(wrapper) { + this.wrapper = $(wrapper); + this.container = this.wrapper.find('.layout-main-section'); + this.container.append($('
')); + } + + show() { + + } +} diff --git a/frappe/core/page/recorder/recorder.json b/frappe/core/page/recorder/recorder.json new file mode 100644 index 0000000000..43dfbc0e09 --- /dev/null +++ b/frappe/core/page/recorder/recorder.json @@ -0,0 +1,23 @@ +{ + "content": null, + "creation": "2019-02-08 08:17:45.392739", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2019-02-08 08:23:04.416426", + "modified_by": "Administrator", + "module": "Core", + "name": "recorder", + "owner": "Administrator", + "page_name": "Recorder", + "roles": [ + { + "role": "Administrator" + } + ], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Recorder" +} \ No newline at end of file diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 125db507ea..0a6f6d4d54 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -119,7 +119,7 @@ frappe.ui.form.on("Customize Form", { frm.set_df_property("sort_field", "options", fields); } - if(frappe.route_options) { + if(frappe.route_options && frappe.route_options.doc_type) { setTimeout(function() { frm.set_value("doc_type", frappe.route_options.doc_type); frappe.route_options = null; diff --git a/frappe/database/database.py b/frappe/database/database.py index 136e25cd93..c9b184cbd1 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -881,9 +881,14 @@ class Database(object): def get_descendants(self, doctype, name): '''Return descendants of the current record''' - lft, rgt = self.get_value(doctype, name, ('lft', 'rgt')) - return self.sql_list('''select name from `tab{doctype}` - where lft > {lft} and rgt < {rgt}'''.format(doctype=doctype, lft=lft, rgt=rgt)) + node_location_indexes = self.get_value(doctype, name, ('lft', 'rgt')) + if node_location_indexes: + lft, rgt = node_location_indexes + return self.sql_list('''select name from `tab{doctype}` + where lft > {lft} and rgt < {rgt}'''.format(doctype=doctype, lft=lft, rgt=rgt)) + else: + # when document does not exist + return [] def is_missing_table_or_column(self, e): return self.is_missing_column(e) or self.is_missing_table(e) diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 30ef20467f..8a95bba228 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -27,7 +27,7 @@ class MariaDBDatabase(Database): self.type_map = { 'Currency': ('decimal', '18,6'), 'Int': ('int', '11'), - 'Long Int': ('bigint', '20'), # convert int to bigint if length is more than 11 + 'Long Int': ('bigint', '20'), 'Float': ('decimal', '18,6'), 'Percent': ('decimal', '18,6'), 'Check': ('int', '1'), diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index fd0e0c72e1..cfbbb80b4c 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -32,7 +32,7 @@ class PostgresDatabase(Database): self.type_map = { 'Currency': ('decimal', '18,6'), 'Int': ('bigint', None), - 'Long Int': ('bigint', None), # convert int to bigint if length is more than 11 + 'Long Int': ('bigint', None), 'Float': ('decimal', '18,6'), 'Percent': ('decimal', '18,6'), 'Check': ('smallint', None), diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.json b/frappe/desk/doctype/desktop_icon/desktop_icon.json index ee9765e944..59c95953ad 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.json +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, @@ -14,6 +15,7 @@ "fields": [ { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -40,10 +42,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -70,10 +74,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -100,10 +106,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -130,10 +138,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -159,10 +169,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -189,10 +201,76 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "description", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Description", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "category", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Category", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -219,10 +297,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -249,10 +329,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -279,10 +361,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -308,10 +392,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -339,10 +425,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -370,10 +458,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -401,10 +491,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -431,10 +523,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -460,10 +554,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -490,10 +586,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -520,10 +618,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -550,10 +650,12 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -580,6 +682,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 } ], @@ -593,7 +696,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-05-08 15:41:31.121652", + "modified": "2019-01-24 04:58:58.720618", "modified_by": "Administrator", "module": "Desk", "name": "Desktop Icon", @@ -602,7 +705,6 @@ "permissions": [ { "amend": 0, - "apply_user_permissions": 0, "cancel": 0, "create": 1, "delete": 1, @@ -629,5 +731,6 @@ "sort_order": "DESC", "title_field": "module_name", "track_changes": 1, - "track_seen": 0 + "track_seen": 0, + "track_views": 0 } \ No newline at end of file diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index 175d1ece7d..fcf10ef61d 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -31,7 +31,7 @@ def get_desktop_icons(user=None): user_icons = frappe.cache().hget('desktop_icons', user) if not user_icons: - fields = ['module_name', 'hidden', 'label', 'link', 'type', 'icon', 'color', + fields = ['module_name', 'hidden', 'label', 'link', 'type', 'icon', 'color', 'description', 'category', '_doctype', '_report', 'idx', 'force_show', 'reverse', 'custom', 'standard', 'blocked'] active_domains = frappe.get_active_domains() diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 0566a4d14b..7a8b059baa 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -94,6 +94,7 @@ def get_docinfo(doc=None, doctype=None, name=None): frappe.response["docinfo"] = { "attachments": get_attachments(doc.doctype, doc.name), "communications": _get_communications(doc.doctype, doc.name), + 'comments': get_comments(doc.doctype, doc.name), 'total_comments': len(json.loads(doc.get('_comments') or '[]')), 'versions': get_versions(doc), "assignments": get_assignments(doc.doctype, doc.name), @@ -120,6 +121,19 @@ def get_communications(doctype, name, start=0, limit=20): return _get_communications(doctype, name, start, limit) +def get_comments(doctype, name): + comments = frappe.get_all('Comment', fields = ['*'], filters = dict( + reference_doctype = doctype, + reference_name = name + )) + + # convert to markdown (legacy ?) + for c in comments: + if c.comment_type == 'Comment': + c.content = frappe.utils.markdown(c.content) + + return comments + def _get_communications(doctype, name, start=0, limit=20): communications = get_communication_data(doctype, name, start, limit) for c in communications: @@ -130,8 +144,6 @@ def _get_communications(doctype, name, start=0, limit=20): "attached_to_name": c.name} )) - elif c.communication_type=="Comment" and c.comment_type=="Comment": - c.content = frappe.utils.markdown(c.content) return communications def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=None, @@ -142,19 +154,15 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields= `communication_date`, `content`, `sender`, `sender_full_name`, `creation`, `subject`, `delivery_status`, `_liked_by`, `timeline_doctype`, `timeline_name`, `reference_doctype`, `reference_name`, - `link_doctype`, `link_name`, `read_by_recipient`, `rating` ''' + `link_doctype`, `link_name`, `read_by_recipient`, `rating`, 'Communication' AS `doctype`''' - conditions = '''communication_type in ('Communication', 'Comment', 'Feedback') + conditions = '''communication_type in ('Communication', 'Feedback') and ( (reference_doctype=%(doctype)s and reference_name=%(name)s) or ( - (timeline_doctype=%(doctype)s and timeline_name=%(name)s) - and ( - communication_type='Communication' - or ( - communication_type='Comment' - and comment_type in ('Created', 'Updated', 'Submitted', 'Cancelled', 'Deleted') - ))) + (timeline_doctype=%(doctype)s and timeline_name=%(name)s) + and (communication_type='Communication') + ) )''' diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 403da16361..a0a33f5d42 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -57,23 +57,23 @@ def validate_link(): frappe.response['message'] = 'Ok' @frappe.whitelist() -def add_comment(doc): +def add_comment(reference_doctype, reference_name, content, comment_email): """allow any logged user to post a comment""" - doc = frappe.get_doc(json.loads(doc)) - - doc.content = clean_email_html(doc.content) - - if not (doc.doctype=="Communication" and doc.communication_type=='Comment'): - frappe.throw(_("This method can only be used to create a Comment"), frappe.PermissionError) - - doc.insert(ignore_permissions=True) + doc = frappe.get_doc(dict( + doctype = 'Comment', + reference_doctype = reference_doctype, + reference_name = reference_name, + content = clean_email_html(content), + comment_email = comment_email, + comment_type = 'Comment' + )).insert(ignore_permissions = True) return doc.as_dict() @frappe.whitelist() def update_comment(name, content): """allow only owner to update comment""" - doc = frappe.get_doc('Communication', name) + doc = frappe.get_doc('Comment', name) if frappe.session.user not in ['Administrator', doc.owner]: frappe.throw(_('Comment can only be edited by the owner'), frappe.PermissionError) diff --git a/frappe/desk/like.py b/frappe/desk/like.py index ec01eace83..e7d1ff298c 100644 --- a/frappe/desk/like.py +++ b/frappe/desk/like.py @@ -64,13 +64,12 @@ def _toggle_like(doctype, name, add, user=None): def remove_like(doctype, name): """Remove previous Like""" # remove Comment - frappe.delete_doc("Communication", [c.name for c in frappe.get_all("Communication", + frappe.delete_doc("Comment", [c.name for c in frappe.get_all("Comment", filters={ - "communication_type": "Comment", + "comment_type": "Like", "reference_doctype": doctype, "reference_name": name, "owner": frappe.session.user, - "comment_type": "Like" } )], ignore_permissions=True) diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py index f5a1e47bf4..43b5a30e64 100644 --- a/frappe/desk/moduleview.py +++ b/frappe/desk/moduleview.py @@ -24,7 +24,7 @@ def hide_module(module): set_hidden(module, frappe.session.user, 1) clear_desktop_icons_cache() -def get_data(module): +def get_data(module, build=True): """Get module data for the module view `desk/#Module/[name]`""" doctype_info = get_doctype_info(module) data = build_config_from_file(module) @@ -40,7 +40,43 @@ def get_data(module): data = combine_common_sections(data) data = apply_permissions(data) - #set_last_modified(data) + # set_last_modified(data) + + if build: + exists_cache = {} + def doctype_contains_a_record(name): + exists = exists_cache.get(name) + if not exists: + if not frappe.db.get_value('DocType', name, 'issingle'): + exists = frappe.db.count(name) + else: + exists = True + exists_cache[name] = exists + return exists + + for section in data: + for item in section["items"]: + # Onboarding + + # First disable based on exists of depends_on list + doctype = item.get("doctype") + dependencies = item.get("dependencies") or None + if not dependencies and doctype: + item["dependencies"] = [doctype] + + dependencies = item.get("dependencies") + if dependencies: + incomplete_dependencies = [d for d in dependencies if not doctype_contains_a_record(d)] + if len(incomplete_dependencies): + item["incomplete_dependencies"] = incomplete_dependencies + + if item.get("onboard"): + # Mark Spotlights for initial + if item.get("type") == "doctype": + name = item.get("name") + count = doctype_contains_a_record(name) + + item["count"] = count return data @@ -184,14 +220,17 @@ def get_config(app, module): config = frappe.get_module("{app}.config.{module}".format(app=app, module=module)) config = config.get_data() - for section in config: + sections = [s for s in config if s.get("condition", True)] + + for section in sections: for item in section["items"]: if item["type"]=="report" and frappe.db.get_value("Report", item["name"], "disabled")==1: section["items"].remove(item) continue if not "label" in item: item["label"] = _(item["name"]) - return config + + return sections def add_setup_section(config, app, module, label, icon): """Add common sections to `/desk#Module/Setup`""" diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 43b67f2976..c700e8f046 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -251,6 +251,11 @@ def get_open_count(doctype, name, items=[]): :param transactions: List of transactions (json/dict) :param filters: optional filters (json/list)''' + if frappe.flags.in_migrate or frappe.flags.in_install: + return { + 'count': [] + } + frappe.has_permission(doc=frappe.get_doc(doctype, name), throw=True) meta = frappe.get_meta(doctype) diff --git a/frappe/desk/page/activity/activity.js b/frappe/desk/page/activity/activity.js index 4f012512e4..fe81fdf0fa 100644 --- a/frappe/desk/page/activity/activity.js +++ b/frappe/desk/page/activity/activity.js @@ -53,14 +53,7 @@ frappe.pages['activity'].on_page_load = function(wrapper) { } frappe.set_route("List", "Activity Log", "Report"); - }, 'fa fa-th') - - this.page.add_menu_item(__('Show Likes'), function() { - frappe.route_options = { - show_likes: true - }; - me.page.list.refresh(); - }, 'octicon octicon-heart'); + }, 'fa fa-th'); }; frappe.pages['activity'].on_page_show = function() { @@ -158,9 +151,7 @@ frappe.activity.render_heatmap = function(page) { data: {} }); - heatmap.update({ - dataPoints: r.message - }); + heatmap.update(r.message); } } }) @@ -196,8 +187,7 @@ frappe.views.Activity = class Activity extends frappe.views.BaseList { get_args() { return { start: this.start, - page_length: this.page_length, - show_likes: (frappe.route_options || {}).show_likes || 0 + page_length: this.page_length }; } diff --git a/frappe/desk/page/activity/activity.py b/frappe/desk/page/activity/activity.py index 54d643ade2..e25c94d0e5 100644 --- a/frappe/desk/page/activity/activity.py +++ b/frappe/desk/page/activity/activity.py @@ -7,43 +7,45 @@ from frappe.utils import cint from frappe.core.doctype.activity_log.feed import get_feed_match_conditions @frappe.whitelist() -def get_feed(start, page_length, show_likes=False): +def get_feed(start, page_length): """get feed""" - match_conditions = get_feed_match_conditions(frappe.session.user) + match_conditions_communication = get_feed_match_conditions(frappe.session.user, 'Communication') + match_conditions_comment = get_feed_match_conditions(frappe.session.user, 'Comment') result = frappe.db.sql("""select X.* from (select name, owner, modified, creation, seen, comment_type, - reference_doctype, reference_name, link_doctype, link_name, subject, - communication_type, communication_medium, content - from `tabCommunication` + reference_doctype, reference_name, link_doctype, link_name, subject, + communication_type, communication_medium, content + from + `tabCommunication` where - communication_type in ("Communication", "Comment") - and communication_medium != "Email" - and (comment_type is null or comment_type != "Like" - or (comment_type="Like" and (owner=%(user)s or reference_owner=%(user)s))) - {match_conditions} - {show_likes} - union + communication_type = "Communication" + and communication_medium != "Email" + and {match_conditions_communication} + UNION select name, owner, modified, creation, '0', 'Updated', - reference_doctype, reference_name, link_doctype, link_name, subject, - 'Comment', '', content - from `tabActivity Log`) X + reference_doctype, reference_name, link_doctype, link_name, subject, + 'Comment', '', content + from + `tabActivity Log` + UNION + select name, owner, modified, creation, '0', comment_type, + reference_doctype, reference_name, link_doctype, link_name, '', + 'Comment', '', content + from + `tabComment` + where + {match_conditions_comment} + ) X order by X.creation DESC limit %(start)s, %(page_length)s""" - .format(match_conditions="and {0}".format(match_conditions) if match_conditions else "", - show_likes="and comment_type='Like'" if show_likes else ""), - { + .format(match_conditions_comment = match_conditions_comment, + match_conditions_communication = match_conditions_communication), { "user": frappe.session.user, "start": cint(start), "page_length": cint(page_length) }, as_dict=True) - if show_likes: - # mark likes as seen! - frappe.db.sql("""update `tabCommunication` set seen=1 - where comment_type='Like' and reference_owner=%s""", frappe.session.user) - frappe.local.flags.commit = True - return result @frappe.whitelist() diff --git a/frappe/desk/page/modules/modules.js b/frappe/desk/page/modules/modules.js deleted file mode 100644 index 80048fbf59..0000000000 --- a/frappe/desk/page/modules/modules.js +++ /dev/null @@ -1,185 +0,0 @@ -frappe.pages['modules'].on_page_load = function(wrapper) { - var page = frappe.ui.make_app_page({ - parent: wrapper, - title: 'Modules', - single_column: false - }); - - frappe.modules_page = page; - frappe.module_links = {}; - page.section_data = {}; - - page.wrapper.find('.page-head h1').css({'padding-left': '15px'}); - // page.wrapper.find('.page-content').css({'margin-top': '0px'}); - - // menu - page.add_menu_item(__('Set Desktop Icons'), function() { - frappe.frappe_toolbar.modules_select - .show(frappe.session.user); - }); - - if(frappe.user.has_role('System Manager')) { - page.add_menu_item(__('Install Apps'), function() { - frappe.set_route("applications"); - }); - } - - page.get_page_modules = () => { - return frappe.get_desktop_icons(true) - .filter(d => d.type==='module' && !d.blocked) - .sort((a, b) => { return (a._label > b._label) ? 1 : -1; }); - }; - - let get_module_sidebar_item = (item) => `
  • - - - ${item._label} - -
  • `; - - let get_sidebar_html = () => { - let sidebar_items_html = page.get_page_modules() - .map(get_module_sidebar_item.bind(this)).join(""); - - return ``; - }; - - // render sidebar - page.sidebar.html(get_sidebar_html()); - - // help click - page.main.on("click", '.module-section-link[data-type="help"]', function() { - frappe.help.show_video($(this).attr("data-youtube-id")); - return false; - }); - - // notifications click - page.main.on("click", '.open-notification', function() { - var doctype = $(this).attr('data-doctype'); - if(doctype) { - frappe.ui.notifications.show_open_count_list(doctype); - } - }); - - page.activate_link = function(link) { - page.last_link = link; - page.wrapper.find('.module-sidebar-item.active, .module-link.active').removeClass('active'); - $(link).addClass('active').parent().addClass("active"); - show_section($(link).attr('data-name')); - }; - - var show_section = function(module_name) { - if (!module_name) return; - if(module_name in page.section_data) { - render_section(page.section_data[module_name]); - } else { - page.main.empty(); - return frappe.call({ - method: "frappe.desk.moduleview.get", - args: { - module: module_name - }, - callback: function(r) { - var m = frappe.get_module(module_name); - m.data = r.message.data; - process_data(module_name, m.data); - page.section_data[module_name] = m; - render_section(m); - }, - freeze: true, - }); - } - - }; - - var render_section = function(m) { - page.set_title(__(m.label)); - page.main.html(frappe.render_template('modules_section', m)); - - // if(frappe.utils.is_xs() || frappe.utils.is_sm()) { - // // call this after a timeout, becuase a refresh will set the page to the top - // setTimeout(function() { - // $(document).scrollTop($('.module-body').offset().top - 150); - // }, 100); - // } - - //setup_section_toggle(); - frappe.app.update_notification_count_in_modules(); - }; - - var process_data = function(module_name, data) { - frappe.module_links[module_name] = []; - data.forEach(function(section) { - section.items.forEach(function(item) { - item.style = ''; - if(item.type==="doctype") { - item.doctype = item.name; - - // map of doctypes that belong to a module - frappe.module_links[module_name].push(item.name); - } - if(!item.route) { - if(item.link) { - item.route=strip(item.link, "#"); - } - else if(item.type==="doctype") { - if(frappe.model.is_single(item.doctype)) { - item.route = 'Form/' + item.doctype; - } else { - if (item.filters) { - frappe.route_options=item.filters; - } - item.route="List/" + item.doctype; - //item.style = 'font-weight: 500;'; - } - // item.style = 'font-weight: bold;'; - } - else if(item.type==="report" && item.is_query_report) { - item.route="query-report/" + item.name; - } - else if(item.type==="report") { - item.route="List/" + item.doctype + "/Report/" + item.name; - } - else if(item.type==="page") { - item.route=item.name; - } - } - - if(item.route_options) { - item.route += "?" + $.map(item.route_options, function(value, key) { - return encodeURIComponent(key) + "=" + encodeURIComponent(value); }).join('&'); - } - - if(item.type==="page" || item.type==="help" || item.type==="report" || - (item.doctype && frappe.model.can_read(item.doctype))) { - item.shown = true; - } - }); - }); - }; -}; - -frappe.pages['modules'].on_page_show = function(wrapper) { - let route = frappe.get_route(); - let modules = frappe.modules_page.get_page_modules().map(d => d.module_name); - $("body").attr("data-sidebar", 1); - if(route.length > 1) { - // activate section based on route - let module_name = route[1]; - if(modules.includes(module_name)) { - frappe.modules_page.activate_link( - frappe.modules_page.sidebar.find('.module-link[data-name="'+ module_name +'"]')); - } else { - frappe.throw(__(`Module ${module_name} not found.`)); - } - } else if(frappe.modules_page.last_link) { - // open last link - frappe.set_route('modules', frappe.modules_page.last_link.attr('data-name')); - } else { - // first time, open the first page - frappe.modules_page.activate_link(frappe.modules_page.sidebar.find('.module-link:first')); - } -}; diff --git a/frappe/desk/page/modules/modules.json b/frappe/desk/page/modules/modules.json deleted file mode 100644 index 1858a921b2..0000000000 --- a/frappe/desk/page/modules/modules.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "content": null, - "creation": "2016-03-07 04:46:00.420330", - "docstatus": 0, - "doctype": "Page", - "idx": 0, - "modified": "2016-03-07 04:46:00.420330", - "modified_by": "Administrator", - "module": "Desk", - "name": "modules", - "owner": "Administrator", - "page_name": "modules", - "roles": [], - "script": null, - "standard": "Yes", - "style": null, - "title": "Modules" -} \ No newline at end of file diff --git a/frappe/desk/page/modules/modules_section.html b/frappe/desk/page/modules/modules_section.html deleted file mode 100644 index 5d23db795a..0000000000 --- a/frappe/desk/page/modules/modules_section.html +++ /dev/null @@ -1,30 +0,0 @@ -
    -{% for (var i=0; i < data.length; i++) { var section = data[i]; %} -{% if ((i % 2)===0) { %}
    {% } %} -
    -
    - {{ section.label }} -
    -
    - {% for (var j=0; j < section.items.length; j++) { - var item = section.items[j]; - if(item.shown) { %} -
    - - {{ item.label || __(item.name) }} - - {% if(item.type==="doctype") { %} - - {% } %} -
    - {% } %} - {% } %} -
    -
    -{% if ((i % 2)===1 || i===data.length-1) { %}
    {% } %} -{% } %} -
    diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 8954a9d36c..82b16da891 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -190,30 +190,34 @@ def run(report_name, filters=None, user=None): def get_prepared_report_result(report, filters, dn="", user=None): latest_report_data = {} - # Only look for completed prepared reports with given filters. - doc_list = frappe.get_all("Prepared Report", - filters={"status": "Completed", "report_name": report.name, "filters": filters, "owner": user}) - doc = None - if len(doc_list): - if dn: - # Get specified dn - doc = frappe.get_doc("Prepared Report", dn) - else: + if dn: + # Get specified dn + doc = frappe.get_doc("Prepared Report", dn) + else: + # Only look for completed prepared reports with given filters. + doc_list = frappe.get_all("Prepared Report", filters={"status": "Completed", "filters": json.dumps(filters), "owner": user}) + if doc_list: # Get latest doc = frappe.get_doc("Prepared Report", doc_list[0]) - # Prepared Report data is stored in a GZip compressed JSON file - attached_file_name = frappe.db.get_value("File", {"attached_to_doctype": doc.doctype, "attached_to_name":doc.name}, "name") - attached_file = frappe.get_doc('File', attached_file_name) - compressed_content = attached_file.get_content() - uncompressed_content = gzip_decompress(compressed_content) - data = json.loads(uncompressed_content) - if data: - latest_report_data = { - "columns": json.loads(doc.columns) if doc.columns else data[0], - "result": data - } + if doc: + try: + # Prepared Report data is stored in a GZip compressed JSON file + attached_file_name = frappe.db.get_value("File", {"attached_to_doctype": doc.doctype, "attached_to_name":doc.name}, "name") + attached_file = frappe.get_doc('File', attached_file_name) + compressed_content = attached_file.get_content() + uncompressed_content = gzip_decompress(compressed_content) + data = json.loads(uncompressed_content) + if data: + latest_report_data = { + "columns": json.loads(doc.columns) if doc.columns else data[0], + "result": data + } + except Exception: + frappe.delete_doc("Prepared Report", doc.name) + frappe.db.commit() + doc = None latest_report_data.update({ "prepared_report": True, diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 362f883581..42409867fb 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -27,7 +27,9 @@ def get_form_params(): """Stringify GET request parameters.""" data = frappe._dict(frappe.local.form_dict) - del data["cmd"] + data.pop('cmd', None) + data.pop('data', None) + if "csrf_token" in data: del data["csrf_token"] @@ -212,23 +214,27 @@ def delete_items(): """delete selected items""" import json - il = sorted(json.loads(frappe.form_dict.get('items')), reverse=True, key=frappe.safe_decode) + items = sorted(json.loads(frappe.form_dict.get('items')), reverse=True) doctype = frappe.form_dict.get('doctype') - failed = [] + if len(items) > 10: + frappe.enqueue('frappe.desk.reportview.delete_bulk', + doctype=doctype, items=items) + else: + delete_bulk(doctype, items) - for i, d in enumerate(il): +def delete_bulk(doctype, items): + failed = [] + for i, d in enumerate(items): try: frappe.delete_doc(doctype, d) - if len(il) >= 5: + if len(items) >= 5: frappe.publish_realtime("progress", - dict(progress=[i+1, len(il)], title=_('Deleting {0}').format(doctype), description=d), + dict(progress=[i+1, len(items)], title=_('Deleting {0}').format(doctype), description=d), user=frappe.session.user) except Exception: failed.append(d) - return failed - @frappe.whitelist() @frappe.read_only() def get_sidebar_stats(stats, doctype, filters=[]): diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json index 5ab3cd1630..ce9b0f30b0 100644 --- a/frappe/email/doctype/email_account/email_account.json +++ b/frappe/email/doctype/email_account/email_account.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 1, @@ -1063,6 +1064,40 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "enable_outgoing", + "description": "Uses the Email Address Name mentioned in this Account as the Sender's Name for all emails sent using this Account.", + "fieldname": "always_use_account_name_as_sender_name", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Always use Account's Name as Sender's Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1563,7 +1598,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-01-30 11:02:41.011412", + "modified": "2019-02-12 17:09:50.653403", "modified_by": "Administrator", "module": "Email", "name": "Email Account", diff --git a/frappe/email/doctype/email_group_member/email_group_member.py b/frappe/email/doctype/email_group_member/email_group_member.py index d0968425d0..23b279e755 100644 --- a/frappe/email/doctype/email_group_member/email_group_member.py +++ b/frappe/email/doctype/email_group_member/email_group_member.py @@ -7,7 +7,13 @@ import frappe from frappe.model.document import Document class EmailGroupMember(Document): - pass + def after_delete(self): + email_group = frappe.get_doc('Email Group', self.email_group) + email_group.update_total_subscribers() + + def after_insert(self): + email_group = frappe.get_doc('Email Group', self.email_group) + email_group.update_total_subscribers() def after_doctype_insert(): - frappe.db.add_unique("Email Group Member", ("email_group", "email")) \ No newline at end of file + frappe.db.add_unique("Email Group Member", ("email_group", "email")) diff --git a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py index 2f879f98ee..e532e2b7eb 100644 --- a/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py +++ b/frappe/email/doctype/email_unsubscribe/email_unsubscribe.py @@ -35,5 +35,5 @@ class EmailUnsubscribe(Document): def on_update(self): if self.reference_doctype and self.reference_name: doc = frappe.get_doc(self.reference_doctype, self.reference_name) - doc.add_comment("Label", _("Left this conversation"), comment_by=self.email) + doc.add_comment("Label", _("Left this conversation"), comment_email=self.email) diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 486b44bea5..ba211bdf23 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -216,8 +216,15 @@ def get_context(context): please enable Allow Print For {0} in Print Settings""".format(status)), title=_("Error in Notification")) else: - return [{"print_format_attachment":1, "doctype":doc.doctype, "name": doc.name, - "print_format":self.print_format, "print_letterhead": print_settings.with_letterhead}] + return [{ + "print_format_attachment": 1, + "doctype": doc.doctype, + "name": doc.name, + "print_format": self.print_format, + "print_letterhead": print_settings.with_letterhead, + "lang": frappe.db.get_value('Print Format', self.print_format, 'default_print_language') + if self.print_format else 'en' + }] def get_template(self): diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index b4af93e61d..4904f60831 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -174,6 +174,7 @@ class EMail: self.reply_to = validate_email_add(strip(self.reply_to) or self.sender, True) self.replace_sender() + self.replace_sender_name() self.recipients = [strip(r) for r in self.recipients] self.cc = [strip(r) for r in self.cc] @@ -188,6 +189,12 @@ class EMail: sender_name, sender_email = parse_addr(self.sender) self.sender = email.utils.formataddr((str(Header(sender_name or self.email_account.name, 'utf-8')), self.email_account.email_id)) + def replace_sender_name(self): + if cint(self.email_account.always_use_account_name_as_sender_name): + self.set_header('X-Original-From', self.sender) + sender_name, sender_email = parse_addr(self.sender) + self.sender = email.utils.formataddr((str(Header(self.email_account.name, 'utf-8')), sender_email)) + def set_message_id(self, message_id, is_notification=False): if message_id: self.msg_root["Message-Id"] = '<' + message_id + '>' diff --git a/frappe/email/queue.py b/frappe/email/queue.py index ba66e670dd..a7988bc46e 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -174,7 +174,8 @@ def get_email_queue(recipients, sender, subject, **kwargs): if att.get('fid'): _attachments.append(att) elif att.get("print_format_attachment") == 1: - att['lang'] = frappe.local.lang + if not att.get('lang', None): + att['lang'] = frappe.local.lang att['print_letterhead'] = kwargs.get('print_letterhead') _attachments.append(att) e.attachments = json.dumps(_attachments) diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 9bdf3a9ba3..09aa4ff57a 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -190,12 +190,10 @@ class EmailServer: # compare the UIDVALIDITY of email account and imap server uid_validity = self.settings.uid_validity - responce, message = self.imap.status("Inbox", "(UIDVALIDITY UIDNEXT)") - current_uid_validity = self.parse_imap_responce("UIDVALIDITY", message[0]) - if not current_uid_validity: - frappe.throw(_("Can not find UIDVALIDITY in imap status response")) + response, message = self.imap.status("Inbox", "(UIDVALIDITY UIDNEXT)") + current_uid_validity = self.parse_imap_response("UIDVALIDITY", message[0]) or 0 - uidnext = int(self.parse_imap_responce("UIDNEXT", message[0]) or "1") + uidnext = int(self.parse_imap_response("UIDNEXT", message[0]) or "1") frappe.db.set_value("Email Account", self.settings.email_account, "uidnext", uidnext) if not uid_validity or uid_validity != current_uid_validity: @@ -223,9 +221,9 @@ class EmailServer: elif uid_validity == current_uid_validity: return - def parse_imap_responce(self, cmd, responce): + def parse_imap_response(self, cmd, response): pattern = r"(?<={cmd} )[0-9]*".format(cmd=cmd) - match = re.search(pattern, responce.decode('utf-8'), re.U | re.I) + match = re.search(pattern, response.decode('utf-8'), re.U | re.I) if match: return match.group(0) else: diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index 549b3f1d1e..99b5f94bf0 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -109,7 +109,8 @@ def get_default_outgoing_email_account(raise_exception_not_set=True): "mail_password": "Super.Secret.Password", "auto_email_id": "emails@example.com", "email_sender_name": "Example Notifications", - "always_use_account_email_id_as_sender": 0 + "always_use_account_email_id_as_sender": 0, + "always_use_account_name_as_sender_name": 0 } ''' email_account = _get_email_account({"enable_outgoing": 1, "default_outgoing": 1}) @@ -128,7 +129,8 @@ def get_default_outgoing_email_account(raise_exception_not_set=True): "login_id": frappe.conf.get("mail_login"), "email_id": frappe.conf.get("auto_email_id") or frappe.conf.get("mail_login") or 'notifications@example.com', "password": frappe.conf.get("mail_password"), - "always_use_account_email_id_as_sender": frappe.conf.get("always_use_account_email_id_as_sender", 0) + "always_use_account_email_id_as_sender": frappe.conf.get("always_use_account_email_id_as_sender", 0), + "always_use_account_name_as_sender_name": frappe.conf.get("always_use_account_name_as_sender_name", 0) }) email_account.from_site_config = True email_account.name = frappe.conf.get("email_sender_name") or "Frappe" @@ -182,6 +184,7 @@ class SMTPServer: self.use_tls = self.email_account.use_tls self.sender = self.email_account.email_id self.always_use_account_email_id_as_sender = cint(self.email_account.get("always_use_account_email_id_as_sender")) + self.always_use_account_name_as_sender_name = cint(self.email_account.get("always_use_account_name_as_sender_name")) @property def sess(self): diff --git a/frappe/installer.py b/frappe/installer.py index c4af23976f..792d8580e8 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -23,7 +23,7 @@ from frappe.database import setup_database def install_db(root_login="root", root_password=None, db_name=None, source_sql=None, admin_password=None, verbose=True, force=0, site_config=None, reinstall=False, db_type=None): - + if not db_type: db_type = frappe.conf.db_type or 'mariadb' @@ -158,9 +158,6 @@ def remove_app(app_name, dry_run=False, yes=False): if not dry_run: frappe.delete_doc("Module Def", module_name) - # delete desktop icons - frappe.db.sql('delete from `tabDesktop Icon` where app=%s', app_name) - remove_from_installed_apps(app_name) if not dry_run: diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 9364f4403a..8527828404 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -16,6 +16,12 @@ from frappe.utils.password import get_decrypted_password, set_encrypted_password from frappe.utils import (cint, flt, now, cstr, strip_html, getdate, get_datetime, to_timedelta, sanitize_html, sanitize_email, cast_fieldtype) +max_positive_value = { + 'smallint': 2 ** 15, + 'int': 2 ** 31, + 'bigint': 2 ** 63 +} + _classes = {} def get_controller(doctype): @@ -549,20 +555,39 @@ class BaseDocument(object): # single doctype value type is mediumtext return + type_map = frappe.db.type_map + for fieldname, value in iteritems(self.get_valid_dict()): df = self.meta.get_field(fieldname) - if df and df.fieldtype in data_fieldtypes and frappe.db.type_map[df.fieldtype][0]=="varchar": - max_length = cint(df.get("length")) or cint(frappe.db.VARCHAR_LEN) + + if not df or df.fieldtype == 'Check': + # skip standard fields and Check fields + continue + + column_type = type_map[df.fieldtype][0] or None + + if column_type == 'varchar': + default_column_max_length = type_map[df.fieldtype][1] or None + max_length = cint(df.get("length")) or cint(default_column_max_length) if len(cstr(value)) > max_length: - if self.parentfield and self.idx: - reference = _("{0}, Row {1}").format(_(self.doctype), self.idx) + self.throw_length_exceeded_error(df, max_length, value) - else: - reference = "{0} {1}".format(_(self.doctype), self.name) + elif column_type in ('int', 'bigint', 'smallint'): + max_length = max_positive_value[column_type] - frappe.throw(_("{0}: '{1}' ({3}) will get truncated, as max characters allowed is {2}")\ - .format(reference, _(df.label), max_length, value), frappe.CharacterLengthExceededError, title=_('Value too big')) + if abs(value) > max_length: + self.throw_length_exceeded_error(df, max_length, value) + + def throw_length_exceeded_error(self, df, max_length, value): + if self.parentfield and self.idx: + reference = _("{0}, Row {1}").format(_(self.doctype), self.idx) + + else: + reference = "{0} {1}".format(_(self.doctype), self.name) + + frappe.throw(_("{0}: '{1}' ({3}) will get truncated, as max characters allowed is {2}")\ + .format(reference, _(df.label), max_length, value), frappe.CharacterLengthExceededError, title=_('Value too big')) def _validate_update_after_submit(self): # get the full doc with children diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index 76176e3939..f6744c9310 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -12,6 +12,7 @@ import frappe.defaults from frappe.model import data_fieldtypes from frappe.utils import nowdate, nowtime, now_datetime from frappe.core.doctype.user_permission.user_permission import get_user_permissions +from frappe.permissions import get_allowed_docs_for_doctype def get_new_doc(doctype, parent_doc = None, parentfield = None, as_dict=False): if doctype not in frappe.local.new_doc_templates: @@ -53,36 +54,39 @@ def set_user_and_static_default_values(doc): for df in doc.meta.get("fields"): if df.fieldtype in data_fieldtypes: - user_default_value = get_user_default_value(df, defaults, user_permissions) + # user permissions for link options + doctype_user_permissions = user_permissions.get(df.options, []) + # Allowed records for the reference doctype (link field) + allowed_records = get_allowed_docs_for_doctype(doctype_user_permissions, df.parent) + + user_default_value = get_user_default_value(df, defaults, doctype_user_permissions, allowed_records) + if user_default_value is not None: doc.set(df.fieldname, user_default_value) - else: if df.fieldname != doc.meta.title_field: - static_default_value = get_static_default_value(df, user_permissions) + static_default_value = get_static_default_value(df, doctype_user_permissions, allowed_records) if static_default_value is not None: doc.set(df.fieldname, static_default_value) -def get_user_default_value(df, defaults, user_permissions): +def get_user_default_value(df, defaults, doctype_user_permissions, allowed_records): # don't set defaults for "User" link field using User Permissions! if df.fieldtype == "Link" and df.options != "User": # 1 - look in user permissions only for document_type==Setup # We don't want to include permissions of transactions to be used for defaults. - if (frappe.get_meta(df.options).document_type=="Setup" - and user_permissions_exist(df, user_permissions) - and len(user_permissions.get(df.options))==1): - return user_permissions.get(df.options)[0].get("doc") + if frappe.get_meta(df.options).document_type=="Setup" and len(allowed_records)==1: + return allowed_records[0] # 2 - Look in user defaults user_default = defaults.get(df.fieldname) - is_allowed_user_default = user_default and (not user_permissions_exist(df, user_permissions) - or (user_default in user_permissions.get(df.options, []))) + is_allowed_user_default = user_default and (not user_permissions_exist(df, doctype_user_permissions) + or user_default in allowed_records) # is this user default also allowed as per user permissions? if is_allowed_user_default: return user_default -def get_static_default_value(df, user_permissions): +def get_static_default_value(df, doctype_user_permissions, allowed_records): # 3 - look in default of docfield if df.get("default"): if df.default == "__user": @@ -93,8 +97,8 @@ def get_static_default_value(df, user_permissions): elif not df.default.startswith(":"): # a simple default value - is_allowed_default_value = (not user_permissions_exist(df, user_permissions) - or (df.default in user_permissions.get(df.options, []))) + is_allowed_default_value = (not user_permissions_exist(df, doctype_user_permissions) + or (df.default in allowed_records)) if df.fieldtype!="Link" or df.options=="User" or is_allowed_default_value: return df.default @@ -126,10 +130,10 @@ def set_dynamic_default_values(doc, parent_doc, parentfield): if parentfield: doc["parentfield"] = parentfield -def user_permissions_exist(df, user_permissions): +def user_permissions_exist(df, doctype_user_permissions): return (df.fieldtype=="Link" and not getattr(df, "ignore_user_permissions", False) - and df.options in (user_permissions or [])) + and doctype_user_permissions) def get_default_based_on_another_field(df, user_permissions, parent_doc): # default value based on another document @@ -139,7 +143,7 @@ def get_default_based_on_another_field(df, user_permissions, parent_doc): ref_fieldname = ref_doctype.lower().replace(" ", "_") reference_name = parent_doc.get(ref_fieldname) if parent_doc else frappe.db.get_default(ref_fieldname) default_value = frappe.db.get_value(ref_doctype, reference_name, df.fieldname) - is_allowed_default_value = (not user_permissions_exist(df, user_permissions) or + is_allowed_default_value = (not user_permissions_exist(df, user_permissions.get(df.options)) or (default_value in get_allowed_docs_for_doctype(user_permissions[df.options], df.parent))) # is this allowed as per user permissions diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 178bc427eb..9e64b913c3 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -458,7 +458,7 @@ class DatabaseQuery(object): f.operator = '=' value = "" - fallback = '""' + fallback = "''" can_be_null = True if 'ifnull' not in column_name: @@ -747,6 +747,7 @@ def get_list(doctype, *args, **kwargs): '''wrapper for DatabaseQuery''' kwargs.pop('cmd', None) kwargs.pop('ignore_permissions', None) + kwargs.pop('data', None) # If doctype is child table if frappe.is_table(doctype): diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 20e0d00199..153065c5ce 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -195,7 +195,7 @@ def check_if_doc_is_linked(doc, method="Delete"): for item in frappe.db.get_values(link_dt, {link_field:doc.name}, ["name", "parent", "parenttype", "docstatus"], as_dict=True): linked_doctype = item.parenttype if item.parent else link_dt - if linked_doctype in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", 'File', 'Version', "Activity Log"): + if linked_doctype in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", 'File', 'Version', "Activity Log", 'Comment'): # don't check for communication and todo! continue @@ -220,7 +220,7 @@ def check_if_doc_is_linked(doc, method="Delete"): def check_if_doc_is_dynamically_linked(doc, method="Delete"): '''Raise `frappe.LinkExistsError` if the document is dynamically linked''' for df in get_dynamic_link_map().get(doc.doctype, []): - if df.parent in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", 'File', 'Version', 'View Log'): + if df.parent in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", 'File', 'Version', 'View Log', 'Comment'): # don't check for communication and todo! continue @@ -266,59 +266,37 @@ def raise_link_exists_exception(doc, reference_doctype, reference_docname, row=' .format(doc.doctype, doc_link, reference_doctype, reference_link, row), frappe.LinkExistsError) def delete_dynamic_links(doctype, name): - delete_doc("ToDo", frappe.db.sql_list("""select name from `tabToDo` - where reference_type=%s and reference_name=%s""", (doctype, name)), - ignore_permissions=True, force=True) - - frappe.db.sql('''delete from `tabEmail Unsubscribe` - where reference_doctype=%s and reference_name=%s''', (doctype, name)) - - # delete shares - frappe.db.sql("""delete from `tabDocShare` - where share_doctype=%s and share_name=%s""", (doctype, name)) - - # delete versions - frappe.db.sql('delete from tabVersion where ref_doctype=%s and docname=%s', (doctype, name)) - - # delete comments - frappe.db.sql("""delete from `tabCommunication` - where - communication_type = 'Comment' - and reference_doctype=%s and reference_name=%s""", (doctype, name)) - - # delete view logs - frappe.db.sql("""delete from `tabView Log` - where reference_doctype=%s and reference_name=%s""", (doctype, name)) + delete_references('ToDo', doctype, name, 'reference_type') + delete_references('Email Unsubscribe', doctype, name) + delete_references('DocShare', doctype, name, 'share_doctype', 'share_name') + delete_references('Version', doctype, name, 'ref_doctype', 'docname') + delete_references('Comment', doctype, name) + delete_references('View Log', doctype, name) # unlink communications - frappe.db.sql("""update `tabCommunication` - set reference_doctype=null, reference_name=null + clear_references('Communication', doctype, name) + clear_references('Communication', doctype, name, 'link_doctype', 'link_name') + clear_references('Communication', doctype, name, 'timeline_doctype', 'timeline_name') + + clear_references('Activity Log', doctype, name) + clear_references('Activity Log', doctype, name, 'timeline_doctype', 'timeline_name') + +def delete_references(doctype, reference_doctype, reference_name, + reference_doctype_field = 'reference_doctype', reference_name_field = 'reference_name'): + frappe.db.sql('''delete from `tab{0}` + where {1}=%s and {2}=%s'''.format(doctype, reference_doctype_field, reference_name_field), # nosec + (reference_doctype, reference_name)) + +def clear_references(doctype, reference_doctype, reference_name, + reference_doctype_field = 'reference_doctype', reference_name_field = 'reference_name'): + frappe.db.sql('''update + `tab{0}` + set + {1}=NULL, {2}=NULL where - communication_type = 'Communication' - and reference_doctype=%s - and reference_name=%s""", (doctype, name)) + {1}=%s and {2}=%s'''.format(doctype, reference_doctype_field, reference_name_field), # nosec + (reference_doctype, reference_name)) - # unlink secondary references - frappe.db.sql("""update `tabCommunication` - set link_doctype=null, link_name=null - where link_doctype=%s and link_name=%s""", (doctype, name)) - - # unlink feed - frappe.db.sql("""update `tabCommunication` - set timeline_doctype=null, timeline_name=null - where timeline_doctype=%s and timeline_name=%s""", (doctype, name)) - - # unlink activity_log reference_doctype - frappe.db.sql("""update `tabActivity Log` - set reference_doctype=null, reference_name=null - where - reference_doctype=%s - and reference_name=%s""", (doctype, name)) - - # unlink activity_log timeline_doctype - frappe.db.sql("""update `tabActivity Log` - set timeline_doctype=null, timeline_name=null - where timeline_doctype=%s and timeline_name=%s""", (doctype, name)) def insert_feed(doc): from frappe.utils import get_fullname @@ -327,8 +305,7 @@ def insert_feed(doc): return frappe.get_doc({ - "doctype": "Communication", - "communication_type": "Comment", + "doctype": "Comment", "comment_type": "Deleted", "reference_doctype": doc.doctype, "subject": "{0} {1}".format(_(doc.doctype), doc.name), diff --git a/frappe/model/document.py b/frappe/model/document.py index 6bd82ba25a..d6876c7347 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -165,10 +165,10 @@ class Document(BaseDocument): self.latest = frappe.get_doc(self.doctype, self.name) return self.latest - def check_permission(self, permtype='read', permlabel=None): + def check_permission(self, permtype='read', permlevel=None): """Raise `frappe.PermissionError` if not permitted""" if not self.has_permission(permtype): - self.raise_no_permission_to(permlabel or permtype) + self.raise_no_permission_to(permlevel or permtype) def has_permission(self, permtype="read", verbose=False): """Call `frappe.has_permission` if `self.flags.ignore_permissions` @@ -999,7 +999,7 @@ class Document(BaseDocument): frappe.db.commit() def db_get(self, fieldname): - '''get database vale for this fieldname''' + '''get database value for this fieldname''' return frappe.db.get_value(self.doctype, self.name, fieldname) def check_no_back_links_exist(self): @@ -1116,33 +1116,22 @@ class Document(BaseDocument): """Returns Desk URL for this document. `/desk#Form/{doctype}/{name}`""" return "/desk#Form/{doctype}/{name}".format(doctype=self.doctype, name=self.name) - def add_comment(self, comment_type, text=None, comment_by=None, link_doctype=None, link_name=None): + def add_comment(self, comment_type='Comment', text=None, comment_email=None, link_doctype=None, link_name=None, comment_by=None): """Add a comment to this document. :param comment_type: e.g. `Comment`. See Communication for more info.""" - if comment_type=='Comment': - out = frappe.get_doc({ - "doctype":"Communication", - "communication_type": "Comment", - "sender": comment_by or frappe.session.user, - "comment_type": comment_type, - "reference_doctype": self.doctype, - "reference_name": self.name, - "content": text or comment_type, - "link_doctype": link_doctype, - "link_name": link_name - }).insert(ignore_permissions=True) - else: - out = frappe.get_doc(dict( - doctype='Version', - ref_doctype= self.doctype, - docname= self.name, - data = frappe.as_json(dict(comment_type=comment_type, comment=text)) - )) - if comment_by: - out.owner = comment_by - out.insert(ignore_permissions=True) + out = frappe.get_doc({ + "doctype":"Comment", + 'comment_type': comment_type, + "comment_email": comment_email or frappe.session.user, + "comment_by": comment_by, + "reference_doctype": self.doctype, + "reference_name": self.name, + "content": text or comment_type, + "link_doctype": link_doctype, + "link_name": link_name + }).insert(ignore_permissions=True) return out def add_seen(self, user=None): diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 0703a064d3..94a0d0dbfa 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -83,12 +83,10 @@ def sync_customizations(app=None): for app_name in apps: for module_name in frappe.local.app_modules.get(app_name) or []: folder = frappe.get_app_path(app_name, module_name, 'custom') - if os.path.exists(folder): for fname in os.listdir(folder): with open(os.path.join(folder, fname), 'r') as f: data = json.loads(f.read()) - if data.get('sync_on_migrate'): sync_customizations_for_doctype(data, folder) @@ -105,14 +103,31 @@ def sync_customizations_for_doctype(data, folder): # sync single doctype exculding the child doctype def sync_single_doctype(doc_type): - frappe.db.sql('delete from `tab{0}` where `{1}` =%s'.format( - custom_doctype, doctype_fieldname), doc_type) - for d in data[key]: - if d.get(doctype_fieldname) == doc_type: - d['doctype'] = custom_doctype - doc = frappe.get_doc(d) + def _insert(data): + if data.get(doctype_fieldname) == doc_type: + data['doctype'] = custom_doctype + doc = frappe.get_doc(data) doc.db_insert() + if custom_doctype != 'Custom Field': + frappe.db.sql('delete from `tab{0}` where `{1}` =%s'.format( + custom_doctype, doctype_fieldname), doc_type) + + for d in data[key]: + _insert(data) + + else: + for d in data[key]: + field = frappe.db.get_value("Custom Field", {"dt": doc_type, "fieldname": d["fieldname"]}) + if not field: + d["owner"] = "Administrator" + _insert(d) + else: + custom_field = frappe.get_doc("Custom Field", field) + custom_field.flags.ignore_validate = True + custom_field.update(d) + custom_field.db_update() + for doc_type in doctypes: # only sync the parent doctype and child doctype if there isn't any other child table json file if doc_type == doctype or not os.path.exists(os.path.join(folder, frappe.scrub(doc_type)+".json")): diff --git a/frappe/patches.txt b/frappe/patches.txt index 413699c5b3..853aed97fd 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -5,15 +5,16 @@ frappe.patches.v8_0.update_global_search_table frappe.patches.v7_0.update_auth frappe.patches.v8_0.drop_in_dialog #2017-09-22 frappe.patches.v7_2.remove_in_filter +frappe.patches.v11_0.drop_column_apply_user_permissions execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') execute:frappe.reload_doc('core', 'doctype', 'docperm') #2018-05-29 +execute:frappe.reload_doc('core', 'doctype', 'comment') frappe.patches.v8_0.drop_is_custom_from_docperm execute:frappe.reload_doc('core', 'doctype', 'module_def') #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01 frappe.patches.v11_0.replicate_old_user_permissions -frappe.patches.v11_0.drop_column_apply_user_permissions frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03 frappe.patches.v7_1.rename_scheduler_log_to_error_log frappe.patches.v6_1.rename_file_data @@ -145,11 +146,9 @@ frappe.patches.v6_16.feed_doc_owner frappe.patches.v6_21.print_settings_repeat_header_footer frappe.patches.v6_24.set_language_as_code frappe.patches.v6_20x.update_insert_after -finally:frappe.patches.v6_24.sync_desktop_icons frappe.patches.v6_20x.set_allow_draft_for_print frappe.patches.v6_20x.remove_roles_from_website_user frappe.patches.v7_0.set_user_fullname -frappe.patches.v7_0.desktop_icons_hidden_by_admin_as_blocked frappe.patches.v7_0.add_communication_in_doc frappe.patches.v7_0.update_send_after_in_bulk_email execute:frappe.db.sql('''delete from `tabSingles` where doctype="Email Settings"''') # 2016-06-13 @@ -183,15 +182,11 @@ frappe.patches.v8_0.deprecate_integration_broker frappe.patches.v8_0.update_gender_and_salutation frappe.patches.v8_0.setup_email_inbox #2017-03-29 frappe.patches.v8_0.newsletter_childtable_migrate -execute:frappe.db.sql("delete from `tabDesktop Icon` where module_name='Communication'") -execute:frappe.db.sql("update `tabDesktop Icon` set type='list' where _doctype='Communication'") -frappe.patches.v8_0.fix_non_english_desktop_icons # 2017-04-12 frappe.patches.v8_0.set_doctype_values_in_custom_role frappe.patches.v8_0.install_new_build_system_requirements frappe.patches.v8_0.set_currency_field_precision # 2017-05-09 frappe.patches.v8_0.rename_print_to_printing frappe.patches.v7_1.disabled_print_settings_for_custom_print_format -frappe.patches.v8_0.update_desktop_icons execute:frappe.db.sql('update tabReport set module="Desk" where name="ToDo"') frappe.patches.v8_1.enable_allow_error_traceback_in_system_settings frappe.patches.v8_1.update_format_options_in_auto_email_report @@ -236,4 +231,5 @@ frappe.patches.v11_0.fix_order_by_in_reports_json execute:frappe.delete_doc('Page', 'applications', ignore_missing=True) frappe.patches.v11_0.set_missing_creation_and_modified_value_for_user_permissions frappe.patches.v12_0.set_primary_key_in_series -frappe.patches.v12_0.webpage_migrate_description_to_meta_tag \ No newline at end of file +execute:frappe.delete_doc("Page", "modules", ignore_missing=True) +frappe.patches.v11_0.set_default_letter_head_source diff --git a/frappe/patches/v11_0/drop_column_apply_user_permissions.py b/frappe/patches/v11_0/drop_column_apply_user_permissions.py index ed0a6881af..4f46bc0907 100644 --- a/frappe/patches/v11_0/drop_column_apply_user_permissions.py +++ b/frappe/patches/v11_0/drop_column_apply_user_permissions.py @@ -6,8 +6,9 @@ def execute(): to_remove = ['DocPerm', 'Custom DocPerm'] for doctype in to_remove: - if column in frappe.db.get_table_columns(doctype): - frappe.db.sql("alter table `tab{0}` drop column {1}".format(doctype, column)) + if frappe.db.table_exists(doctype): + if column in frappe.db.get_table_columns(doctype): + frappe.db.sql("alter table `tab{0}` drop column {1}".format(doctype, column)) frappe.reload_doc('core', 'doctype', 'docperm', force=True) frappe.reload_doc('core', 'doctype', 'custom_docperm', force=True) diff --git a/frappe/patches/v11_0/set_default_letter_head_source.py b/frappe/patches/v11_0/set_default_letter_head_source.py new file mode 100644 index 0000000000..069f4e3d2e --- /dev/null +++ b/frappe/patches/v11_0/set_default_letter_head_source.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals + +import frappe + +def execute(): + frappe.reload_doctype('Letter Head') + + # source of all existing letter heads must be HTML + frappe.db.sql('update `tabLetter Head` set source = "HTML"') \ No newline at end of file diff --git a/frappe/patches/v12_0/setup_comments_from_communications.py b/frappe/patches/v12_0/setup_comments_from_communications.py new file mode 100644 index 0000000000..2ea6a609b6 --- /dev/null +++ b/frappe/patches/v12_0/setup_comments_from_communications.py @@ -0,0 +1,25 @@ +from __future__ import unicode_literals + +import frappe + +def execute(): + for comment in frappe.get_all('Communication', fields = ['*'], + filters = dict(communication_type = 'Comment')): + + new_comment = frappe.new_doc('Comment') + new_comment.comment_type = comment.comment_type + new_comment.comment_email = comment.sender + new_comment.comment_by = comment.sender_full_name + new_comment.subject = comment.subject + new_comment.reference_doctype = comment.reference_doctype + new_comment.reference_name = comment.reference_name + new_comment.link_doctype = comment.link_doctype + new_comment.link_name = comment.link_name + new_comment.creation = comment.creation + new_comment.modified = comment.modified + new_comment.owner = comment.owner + new_comment.modified_by = comment.modified_by + new_comment.db_insert() + + # clean up + frappe.db.sql('delete from tabCommunication where communication_type = "Comment"') \ No newline at end of file diff --git a/frappe/patches/v6_24/sync_desktop_icons.py b/frappe/patches/v6_24/sync_desktop_icons.py deleted file mode 100644 index 74f52e6056..0000000000 --- a/frappe/patches/v6_24/sync_desktop_icons.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import unicode_literals -import frappe, json - -from frappe.desk.doctype.desktop_icon.desktop_icon import sync_from_app, get_user_copy -import frappe.defaults - -def execute(): - frappe.reload_doc('desk', 'doctype', 'desktop_icon') - - frappe.db.sql('delete from `tabDesktop Icon`') - - modules_list = [] - for app in frappe.get_installed_apps(): - modules_list += sync_from_app(app) - - # sync hidden modules - hidden_modules = frappe.db.get_global('hidden_modules') - if hidden_modules: - for m in json.loads(hidden_modules): - try: - desktop_icon = frappe.get_doc('Desktop Icon', {'module_name': m, 'standard': 1, 'app': app}) - desktop_icon.db_set('hidden', 1) - except frappe.DoesNotExistError: - pass - - # sync user sort - for user in frappe.get_all('User', filters={'user_type': 'System User'}): - user_list = frappe.defaults.get_user_default('_user_desktop_items', user=user.name) - if user_list: - user_list = json.loads(user_list) - for i, module_name in enumerate(user_list): - try: - desktop_icon = get_user_copy(module_name, user=user.name) - desktop_icon.db_set('idx', i) - except frappe.DoesNotExistError: - pass - - # set remaining icons as hidden - for module_name in list(set([m['module_name'] for m in modules_list]) - set(user_list)): - try: - desktop_icon = get_user_copy(module_name, user=user.name) - desktop_icon.db_set('hidden', 1) - except frappe.DoesNotExistError: - pass diff --git a/frappe/patches/v7_0/add_communication_in_doc.py b/frappe/patches/v7_0/add_communication_in_doc.py index 92120634ef..4db02c5bab 100644 --- a/frappe/patches/v7_0/add_communication_in_doc.py +++ b/frappe/patches/v7_0/add_communication_in_doc.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import frappe -from frappe.core.doctype.communication.comment import update_comment_in_doc +from frappe.core.doctype.comment.comment import update_comment_in_doc def execute(): for d in frappe.db.get_all("Communication", diff --git a/frappe/patches/v7_0/desktop_icons_hidden_by_admin_as_blocked.py b/frappe/patches/v7_0/desktop_icons_hidden_by_admin_as_blocked.py deleted file mode 100644 index 496af17cd2..0000000000 --- a/frappe/patches/v7_0/desktop_icons_hidden_by_admin_as_blocked.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import unicode_literals -import frappe - -def execute(): - # all icons hidden in standard are "blocked" - # this is for the use case where the admin wants to remove icon for everyone - - # in 7.0, icons may be hidden by default, but still can be shown to the user - # e.g. Accounts, Stock etc, so we need a new property for blocked - - if frappe.db.table_exists('Desktop Icon'): - frappe.db.sql('update `tabDesktop Icon` set blocked = 1 where standard=1 and hidden=1') \ No newline at end of file diff --git a/frappe/patches/v7_2/merge_knowledge_base.py b/frappe/patches/v7_2/merge_knowledge_base.py index 81ff98230b..301d15e1dd 100644 --- a/frappe/patches/v7_2/merge_knowledge_base.py +++ b/frappe/patches/v7_2/merge_knowledge_base.py @@ -11,12 +11,6 @@ def execute(): update_routes(['Help Category', 'Help Article']) remove_from_installed_apps('knowledge_base') - # remove desktop icon - desktop_icon_name = frappe.db.get_value('Desktop Icon', - dict(module_name='Knowledge Base', type='module')) - if desktop_icon_name: - frappe.delete_doc('Desktop Icon', desktop_icon_name) - # remove module def if frappe.db.exists('Module Def', 'Knowledge Base'): frappe.delete_doc('Module Def', 'Knowledge Base') diff --git a/frappe/patches/v8_0/fix_non_english_desktop_icons.py b/frappe/patches/v8_0/fix_non_english_desktop_icons.py deleted file mode 100644 index b4389578ab..0000000000 --- a/frappe/patches/v8_0/fix_non_english_desktop_icons.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2017, Frappe and Contributors -# License: GNU General Public License v3. See license.txt -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals -import frappe -from frappe.desk.doctype.desktop_icon.desktop_icon import clear_desktop_icons_cache - -def execute(): - frappe.db.sql(""" - update `tabDesktop Icon` - set module_name=_doctype, label=_doctype - where type = 'link' and _doctype != label and link like 'List/%' - """) \ No newline at end of file diff --git a/frappe/patches/v8_0/setup_email_inbox.py b/frappe/patches/v8_0/setup_email_inbox.py index 8cd8b28116..1bfe3b0b74 100644 --- a/frappe/patches/v8_0/setup_email_inbox.py +++ b/frappe/patches/v8_0/setup_email_inbox.py @@ -3,31 +3,18 @@ import frappe, json from frappe.core.doctype.user.user import ask_pass_update, setup_user_email_inbox def execute(): - """ + """ depricate email inbox page if exists remove desktop icon for email inbox page if exists patch to remove Custom DocPerm for communication + add user inbox child table entry for existing email account in not exists """ if frappe.db.exists("Page", "email_inbox"): frappe.delete_doc("Page", "email_inbox") - desktop_icon = frappe.db.get_value("Desktop Icon", { - "module_name": "Email", - "type": "Page", - "link": "email_inbox" - }) - - if desktop_icon: - frappe.delete_doc("Desktop Icon", desktop_icon) - frappe.db.sql("""update `tabCustom DocPerm` set `write`=0, email=1 where parent='Communication'""") - setup_inbox_from_email_account() - -def setup_inbox_from_email_account(): - """ add user inbox child table entry for existing email account in not exists """ - frappe.reload_doc("core", "doctype", "user_email") frappe.reload_doc("email", "doctype", "email_account") @@ -36,4 +23,4 @@ def setup_inbox_from_email_account(): for email_account in email_accounts: setup_user_email_inbox(email_account.get("name"), email_account.get("awaiting_password"), - email_account.get("email_id"), email_account.get("enabled_outgoing")) \ No newline at end of file + email_account.get("email_id"), email_account.get("enabled_outgoing")) diff --git a/frappe/patches/v8_0/update_desktop_icons.py b/frappe/patches/v8_0/update_desktop_icons.py deleted file mode 100644 index ea8527718b..0000000000 --- a/frappe/patches/v8_0/update_desktop_icons.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals -import frappe -from frappe.utils import cstr - -def execute(): - """ update the desktop icons """ - - frappe.reload_doc('desk', 'doctype', 'desktop_icon') - - icons = frappe.get_all("Desktop Icon", filters={ "type": "link" }, fields=["link", "name"]) - - for icon in icons: - # check if report exists - icon_link = icon.get("link", "") or "" - parts = icon_link.split("/") - if not parts: - continue - - report_name = parts[-1] - if "report" in parts[0] and frappe.db.get_value("Report", report_name): - frappe.db.sql(""" update `tabDesktop Icon` set _report='{report_name}' - where name='{name}'""".format(report_name=report_name, name=icon.get("name"))) \ No newline at end of file diff --git a/frappe/permissions.py b/frappe/permissions.py index abe5eca84d..261049c226 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -24,8 +24,10 @@ def print_has_permission_check_logs(func): def inner(*args, **kwargs): frappe.flags['has_permission_check_logs'] = [] result = func(*args, **kwargs) + self_perm_check = True if not kwargs.get('user') else kwargs.get('user') == frappe.session.user # print only if access denied - if not result: + # and if user is checking his own permission + if not result and self_perm_check: msgprint(('
    ').join(frappe.flags['has_permission_check_logs'])) frappe.flags.pop('has_permission_check_logs', None) return result diff --git a/frappe/printing/doctype/letter_head/letter_head.js b/frappe/printing/doctype/letter_head/letter_head.js index f70224565d..ca4dad2d07 100644 --- a/frappe/printing/doctype/letter_head/letter_head.js +++ b/frappe/printing/doctype/letter_head/letter_head.js @@ -3,6 +3,6 @@ frappe.ui.form.on('Letter Head', { refresh: function(frm) { - + frm.flag_public_attachments = true; } }); diff --git a/frappe/printing/doctype/letter_head/letter_head.json b/frappe/printing/doctype/letter_head/letter_head.json index fb10be3f84..1bee220213 100644 --- a/frappe/printing/doctype/letter_head/letter_head.json +++ b/frappe/printing/doctype/letter_head/letter_head.json @@ -1,241 +1,450 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "field:letter_head_name", - "beta": 0, - "creation": "2012-11-22 17:45:46", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 1, + "autoname": "field:letter_head_name", + "beta": 0, + "creation": "2012-11-22 17:45:46", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 0, "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "letter_head_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Letter Head Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "letter_head_name", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "letter_head_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Letter Head Name", + "length": 0, + "no_copy": 0, + "oldfieldname": "letter_head_name", + "oldfieldtype": "Data", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "letter_head_name", - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Disabled", - "length": 0, - "no_copy": 0, - "oldfieldname": "disabled", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "letter_head_name", + "fieldname": "source", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Letter Head Based On", + "length": 0, + "no_copy": 0, + "options": "Image\nHTML", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "letter_head_name", - "description": "Check this to make this the default letter head in all prints", - "fieldname": "is_default", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Is Default", - "length": 0, - "no_copy": 0, - "oldfieldname": "is_default", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.__islocal", - "description": "Letter Head in HTML", - "fieldname": "content", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Content", - "length": 0, - "no_copy": 0, - "oldfieldname": "content", - "oldfieldtype": "Text Editor", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "letter_head_name", + "fieldname": "disabled", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Disabled", + "length": 0, + "no_copy": 0, + "oldfieldname": "disabled", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!doc.__islocal", - "fieldname": "footer", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Footer", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "letter_head_name", + "description": "", + "fieldname": "is_default", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Default Letter Head", + "length": 0, + "no_copy": 0, + "oldfieldname": "is_default", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": "", + "columns": 0, + "depends_on": "eval:doc.letter_head_name && doc.source === 'Image'", + "fieldname": "letter_head_image_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Letter Head Image", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.letter_head_name && doc.source === 'Image'", + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Image", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": "", + "columns": 0, + "depends_on": "eval:doc.source==='HTML' && doc.letter_head_name", + "fieldname": "header_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Header", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:!doc.__islocal && doc.source==='HTML'", + "description": "Letter Head in HTML", + "fieldname": "content", + "fieldtype": "HTML Editor", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Header HTML", + "length": 0, + "no_copy": 0, + "oldfieldname": "content", + "oldfieldtype": "Text Editor", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "footer_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Footer", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:!doc.__islocal", + "description": "Footer will display correctly only in PDF", + "fieldname": "footer", + "fieldtype": "HTML Editor", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Footer HTML", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-font", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 3, - "modified": "2018-04-21 17:23:55.709575", - "modified_by": "Administrator", - "module": "Printing", - "name": "Letter Head", - "owner": "Administrator", + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "fa fa-font", + "idx": 1, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 3, + "modified": "2019-02-12 09:48:26.017783", + "modified_by": "Administrator", + "module": "Printing", + "name": "Letter Head", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "All", - "set_user_permissions": 0, - "share": 0, - "submit": 0, + "amend": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "All", + "set_user_permissions": 0, + "share": 0, + "submit": 0, "write": 0 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0 + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_order": "ASC", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 } \ No newline at end of file diff --git a/frappe/printing/doctype/letter_head/letter_head.py b/frappe/printing/doctype/letter_head/letter_head.py index 3e9ce09c83..1a70ebcb08 100644 --- a/frappe/printing/doctype/letter_head/letter_head.py +++ b/frappe/printing/doctype/letter_head/letter_head.py @@ -3,16 +3,30 @@ from __future__ import unicode_literals import frappe +from frappe.utils import is_image from frappe.model.document import Document class LetterHead(Document): + def before_insert(self): + # for better UX, let user set from attachment + self.source = 'Image' + def validate(self): + self.set_image() if not self.is_default: if not frappe.db.sql("""select count(*) from `tabLetter Head` where ifnull(is_default,0)=1"""): self.is_default = 1 + def set_image(self): + if self.source=='Image': + if self.image and is_image(self.image): + self.content = ''.format(self.image) + frappe.msgprint(frappe._('Header HTML set from attachment {0}').format(self.image), alert = True) + else: + frappe.msgprint(frappe._('Please attach an image file to set HTML'), alert = True, indicator = 'orange') + def on_update(self): self.set_as_default() diff --git a/frappe/printing/doctype/letter_head/test_letter_head.py b/frappe/printing/doctype/letter_head/test_letter_head.py index 9535841817..b69e9924ea 100644 --- a/frappe/printing/doctype/letter_head/test_letter_head.py +++ b/frappe/printing/doctype/letter_head/test_letter_head.py @@ -7,4 +7,14 @@ import frappe import unittest class TestLetterHead(unittest.TestCase): - pass + def test_auto_image(self): + letter_head = frappe.get_doc(dict( + doctype = 'Letter Head', + letter_head_name = 'Test', + source = 'Image', + image = '/public/test.png' + )).insert() + + # test if image is automatically set + self.assertTrue(letter_head.image in letter_head.content) + diff --git a/frappe/public/build.json b/frappe/public/build.json index 8798c0d074..9c395c9bca 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -25,6 +25,9 @@ "js/frappe-vue.min.js": [ "public/js/frappe_vue.js" ], + "js/frappe-recorder.min.js": [ + "public/js/frappe/recorder/recorder.js" + ], "js/frappe-web.min.js": [ "public/js/frappe/class.js", "public/js/frappe/polyfill.js", @@ -140,6 +143,7 @@ "public/js/lib/Sortable.min.js", "public/js/lib/jquery/jquery.hotkeys.js", "public/js/lib/bootstrap.min.js", + "node_modules/vue/dist/vue.js", "node_modules/moment/min/moment-with-locales.min.js", "node_modules/moment-timezone/builds/moment-timezone-with-data.min.js", "public/js/lib/socket.io.min.js", @@ -236,7 +240,6 @@ "public/js/frappe/ui/toolbar/about.js", "public/js/frappe/ui/toolbar/navbar.html", "public/js/frappe/ui/toolbar/toolbar.js", - "public/js/frappe/ui/toolbar/modules_select.js", "public/js/frappe/ui/toolbar/notifications.js", "public/js/frappe/views/communication.js", "public/js/frappe/views/translation_manager.js", @@ -390,5 +393,8 @@ ], "js/social.min.js": [ "public/js/frappe/social/social_home.js" + ], + "js/modules.min.js": [ + "public/js/frappe/views/modules_home.js" ] } diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 68ff55efe5..d0464d46cd 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -72,7 +72,7 @@ frappe.Application = Class.extend({ frappe.msgprint(frappe.boot.messages); } - if (frappe.boot.change_log && frappe.boot.change_log.length) { + if (frappe.boot.change_log && frappe.boot.change_log.length && !window.Cypress) { this.show_change_log(); } else { this.show_notes(); @@ -194,7 +194,7 @@ frappe.Application = Class.extend({ load_bootinfo: function() { if(frappe.boot) { frappe.modules = {}; - frappe.boot.desktop_icons.forEach(function(m) { + frappe.boot.allowed_modules.forEach(function(m) { frappe.modules[m.module_name]=m; }); frappe.model.sync(frappe.boot.docs); @@ -264,9 +264,6 @@ frappe.Application = Class.extend({ $.extend(frappe.boot.notification_info, r.message); $(document).trigger("notification-update"); - // update in module views - me.update_notification_count_in_modules(); - if(frappe.get_route()[0] != "messages") { if(r.message.new_messages.length) { frappe.utils.set_title_prefix("(" + r.message.new_messages.length + ")"); @@ -279,18 +276,6 @@ frappe.Application = Class.extend({ } }, - update_notification_count_in_modules: function() { - $.each(frappe.boot.notification_info.open_count_doctype, function(doctype, count) { - if(count) { - $('.open-notification.global[data-doctype="'+ doctype +'"]') - .removeClass("hide").html(count > 99 ? "99+" : count); - } else { - $('.open-notification.global[data-doctype="'+ doctype +'"]') - .addClass("hide"); - } - }); - }, - set_globals: function() { frappe.session.user = frappe.boot.user.name; frappe.session.user_email = frappe.boot.user.email; @@ -412,6 +397,7 @@ frappe.Application = Class.extend({ } }); dialog.set_primary_action(__('Login'), () => { + dialog.set_message(__('Authenticating...')); frappe.call({ method: 'login', args: { @@ -591,86 +577,3 @@ frappe.get_module = function(m, default_module) { return module; }; - -frappe.get_desktop_icons = function(show_hidden, show_global) { - // filter valid icons - - // hidden == hidden from desktop - // blocked == no view from modules either - - var out = []; - - var add_to_out = function(module) { - module = frappe.get_module(module.module_name, module); - module.app_icon = frappe.ui.app_icon.get_html(module); - out.push(module); - }; - - var show_module = function(m) { - var out = true; - if(m.type==="page") { - out = m.link in frappe.boot.page_info; - } else if(m.force_show) { - out = true; - } else if(m._report) { - out = m._report in frappe.boot.user.all_reports; - } else if(m._doctype) { - //out = frappe.model.can_read(m._doctype); - out = frappe.boot.user.can_read.includes(m._doctype); - } else { - if(['Help', 'Settings'].includes(m.module_name)) { - // no permissions necessary for learn - out = true; - } else if(m.module_name==='Setup' && frappe.user.has_role('System Manager')) { - out = true; - } else { - out = frappe.boot.user.allow_modules.indexOf(m.module_name) !== -1; - } - } - if(m.hidden && !show_hidden) { - out = false; - } - if(m.blocked && !show_global) { - out = false; - } - return out; - }; - - let m; - for (var i=0, l=frappe.boot.desktop_icons.length; i < l; i++) { - m = frappe.boot.desktop_icons[i]; - if ((['Setup', 'Core'].indexOf(m.module_name) === -1) && show_module(m)) { - add_to_out(m); - } - } - - if(frappe.user_roles.includes('System Manager')) { - m = frappe.get_module('Setup'); - if(show_module(m)) add_to_out(m); - } - - if(frappe.user_roles.includes('Administrator')) { - m = frappe.get_module('Core'); - if(show_module(m)) add_to_out(m); - } - - return out; -}; - -frappe.add_to_desktop = function(label, doctype, report) { - frappe.call({ - method: 'frappe.desk.doctype.desktop_icon.desktop_icon.add_user_icon', - args: { - 'link': frappe.get_route_str(), - 'label': label, - 'type': 'link', - '_doctype': doctype, - '_report': report - }, - callback: function(r) { - if(r.message) { - frappe.show_alert(__("Added")); - } - } - }); -}; diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index 4c584c1241..39e576ebf8 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -232,6 +232,16 @@ frappe.dom = { frappe.ui.scroll(section.parent().parent()); } }, 200); + }, + pixel_to_inches(pixels) { + const div = $('
    '); + div.appendTo(document.body); + + const dpi_x = document.getElementById('dpi').offsetWidth; + const inches = pixels / dpi_x; + div.remove(); + + return inches; } }; diff --git a/frappe/public/js/frappe/form/controls/check.js b/frappe/public/js/frappe/form/controls/check.js index b54571b166..a9d2e78264 100644 --- a/frappe/public/js/frappe/form/controls/check.js +++ b/frappe/public/js/frappe/form/controls/check.js @@ -1,16 +1,16 @@ frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({ input_type: "checkbox", make_wrapper: function() { - this.$wrapper = $('
    \ -
    \ - \ -

    \ -
    \ -
    ').appendTo(this.parent); + this.$wrapper = $(`
    +
    + +

    +
    +
    `).appendTo(this.parent); }, set_input_areas: function() { this.label_area = this.label_span = this.$wrapper.find(".label-area").get(0); diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 7ba1b0b822..d68f6254b9 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -16,7 +16,7 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlCode.extend({ if ($input_wrapper.is(':visible')) { this.make_map(); } else { - $(document).on('quick-entry-dialog-open', () => { + $(document).on('frappe.ui.Dialog:shown', () => { this.make_map(); }); } diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 1d413b83f8..86a99c970f 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -157,7 +157,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ me.set_custom_query(args); frappe.call({ - type: "GET", + type: "POST", method:'frappe.desk.search.search_link', no_spinner: true, args: args, diff --git a/frappe/public/js/frappe/form/controls/multicheck.js b/frappe/public/js/frappe/form/controls/multicheck.js index 35e1a1a19b..5e8c437445 100644 --- a/frappe/public/js/frappe/form/controls/multicheck.js +++ b/frappe/public/js/frappe/form/controls/multicheck.js @@ -86,7 +86,9 @@ frappe.ui.form.ControlMultiCheck = frappe.ui.form.Control.extend({ this.selected_options.push(option_name); } else { let index = this.selected_options.indexOf(option_name); - this.selected_options.splice(index, 1); + if(index > -1) { + this.selected_options.splice(index, 1); + } } this.df.on_change && this.df.on_change(); }); diff --git a/frappe/public/js/frappe/form/controls/multiselect.js b/frappe/public/js/frappe/form/controls/multiselect.js index ae6a7a124e..bc4f09468b 100644 --- a/frappe/public/js/frappe/form/controls/multiselect.js +++ b/frappe/public/js/frappe/form/controls/multiselect.js @@ -32,6 +32,31 @@ frappe.ui.form.ControlMultiSelect = frappe.ui.form.ControlAutocomplete.extend({ }); }, + get_value() { + let data = this._super(); + // find value of label from option list and return actual value string + if (this.df.options && this.df.options[0].label) { + data = data.split(',').map(op => op.trim()); + data = data.map(val => { + let option = this.df.options.find(op => op.label === val); + return option ? option.value : null; + }).filter(n => n != null).join(', '); + } + return data; + }, + + set_formatted_input(value) { + if (!value) return; + // find label of value from option list and set from it as input + if (this.df.options && this.df.options[0].label) { + value = value.split(',').map(d => d.trim()).map(val => { + let option = this.df.options.find(op => op.value === val); + return option ? option.label : val; + }).filter(n => n != null).join(', '); + } + this._super(value); + }, + get_values() { const value = this.get_value() || ''; const values = value.split(/\s*,\s*/).filter(d => d); diff --git a/frappe/public/js/frappe/form/controls/signature.js b/frappe/public/js/frappe/form/controls/signature.js index 315a8ccfe8..82dcb8f341 100644 --- a/frappe/public/js/frappe/form/controls/signature.js +++ b/frappe/public/js/frappe/form/controls/signature.js @@ -7,7 +7,14 @@ frappe.ui.form.ControlSignature = frappe.ui.form.ControlData.extend({ // make jSignature field this.body = $('
    ').appendTo(me.wrapper); - this.make_pad(); + + if (this.body.is(':visible')) { + this.make_pad(); + } else { + $(document).on('frappe.ui.Dialog:shown', () => { + this.make_pad(); + }); + } this.img_wrapper = $(`
    diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 86f85971c5..3304c574ee 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -28,7 +28,7 @@ frappe.ui.form.Timeline = class Timeline { render_input: true, only_input: true, on_submit: (val) => { - val && this.insert_comment("Comment", val, this.comment_area.button); + val && this.insert_comment(val, this.comment_area.button); } }); @@ -149,10 +149,17 @@ frappe.ui.form.Timeline = class Timeline { this.wrapper.toggle(true); this.list.empty(); this.comment_area.set_value(''); - let communications = this.get_communications(true); - var views = this.get_view_logs(); + // get all communications + let communications = this.get_communications(true); + + // append views + var views = this.get_view_logs(); var timeline = communications.concat(views); + + // append comments + timeline = timeline.concat(this.get_comments()); + // sort timeline .filter(a => a.content) .sort((b, c) => me.compare_dates(b, c)) @@ -312,7 +319,7 @@ frappe.ui.form.Timeline = class Timeline { c["delete"] = ""; c["edit"] = ""; if(c.communication_type=="Comment" && (c.comment_type || "Comment") === "Comment") { - if(frappe.model.can_delete("Communication")) { + if(frappe.model.can_delete("Comment")) { c["delete"] = ''; } @@ -498,6 +505,22 @@ frappe.ui.form.Timeline = class Timeline { return out; } + get_comments() { + let docinfo = this.frm.get_docinfo(); + + for (let c of docinfo.comments) { + this.cast_comment_as_communication(c); + } + + return docinfo.comments; + } + + cast_comment_as_communication(c) { + c.sender = c.comment_email; + c.sender_full_name = c.comment_by; + c.communication_type = 'Comment'; + } + build_version_comments(docinfo, out) { var me = this; docinfo.versions.forEach(function(version) { @@ -530,8 +553,8 @@ frappe.ui.form.Timeline = class Timeline { if(field_display_status === 'Read' || field_display_status === 'Write') { parts.push(__('{0} from {1} to {2}', [ __(df.label), - (frappe.ellipsis(p[1], 40) || '""').bold(), - (frappe.ellipsis(p[2], 40) || '""').bold() + (frappe.ellipsis(frappe.utils.html2text(p[1]), 40) || '""').bold(), + (frappe.ellipsis(frappe.utils.html2text(p[2]), 40) || '""').bold() ])); } } @@ -539,7 +562,7 @@ frappe.ui.form.Timeline = class Timeline { return parts.length < 3; }); if(parts.length) { - out.push(me.get_version_comment(version, __("changed value of {0}", [parts.join(', ')]))); + out.push(me.get_version_comment(version, __("changed value of {0}", [parts.join(', ').bold()]))); } } @@ -616,42 +639,22 @@ frappe.ui.form.Timeline = class Timeline { }; } - insert_comment(comment_type, comment, btn) { + insert_comment(comment, btn) { var me = this; return frappe.call({ method: "frappe.desk.form.utils.add_comment", args: { - doc:{ - doctype: "Communication", - communication_type: "Comment", - comment_type: comment_type || "Comment", - reference_doctype: this.frm.doctype, - reference_name: this.frm.docname, - content: comment, - sender: frappe.session.user - } + reference_doctype: this.frm.doctype, + reference_name: this.frm.docname, + content: comment, + comment_email: frappe.session.user }, btn: btn, callback: function(r) { if(!r.exc) { me.comment_area.set_value(''); frappe.utils.play_sound("click"); - - var comment = r.message; - var comments = me.get_communications(); - var comment_exists = false; - for (var i=0, l=comments.length; i 11.69) { + $message.text(__('This may get printed on multiple pages')); + } else { + $message.text(''); + } }); }, show_footer: function() { // footer is hidden by default as reqd by pdf generation // simple hack to show it in print preview + this.wrapper.find('.print-format').css({ + display: 'flex', + flexDirection: 'column' + }); this.wrapper.find('.page-break').css({ 'display': 'flex', - 'flex-direction': 'column' + 'flex-direction': 'column', + 'flex': '1' }); this.wrapper.find('#footer-html').attr('style', ` display: block !important; order: 1; - margin-top: 20px; + margin-top: auto; `); }, printit: function () { diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index a0cd22163e..257b6cd3ea 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -116,9 +116,6 @@ frappe.ui.form.QuickEntryForm = Class.extend({ this.dialog.onhide = () => frappe.quick_entry = null; this.dialog.show(); - this.dialog.$wrapper.on('shown.bs.modal', function() { - $(document).trigger('quick-entry-dialog-open'); - }); this.dialog.refresh_dependency(); this.set_defaults(); diff --git a/frappe/public/js/frappe/form/templates/print_layout.html b/frappe/public/js/frappe/form/templates/print_layout.html index bbdf415b17..b2abd11099 100644 --- a/frappe/public/js/frappe/form/templates/print_layout.html +++ b/frappe/public/js/frappe/form/templates/print_layout.html @@ -31,6 +31,7 @@ +
    diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 99d913f1cd..44898c3b5e 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -153,13 +153,6 @@ frappe.ui.form.Toolbar = Class.extend({ this.page.add_menu_item(__("Reload"), function() { me.frm.reload_doc();}, true); - // add to desktop - if(me.frm.meta.issingle) { - this.page.add_menu_item(__('Add to Desktop'), function () { - frappe.add_to_desktop(me.frm.doctype, me.frm.doctype); - }, true); - } - // delete if((cint(me.frm.doc.docstatus) != 1) && !me.frm.doc.__islocal && frappe.model.can_delete(me.frm.doctype)) { diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 04286dff20..d92ca100ce 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -104,7 +104,9 @@ frappe.views.BaseList = class BaseList { } return f; }); - //de-dup + // remove null or undefined values + this.fields = this.fields.filter(Boolean); + //de-duplicate this.fields = this.fields.uniqBy(f => f[0] + f[1]); } diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 6052baa6c4..89133df515 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -151,6 +151,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { ); fields.forEach(f => this._add_field(f)); + + this.fields.forEach(f => { + const df = frappe.meta.get_docfield(f[1], f[0]); + if (df && df.fieldtype === 'Currency' && df.options && !df.options.includes(':')) { + this._add_field(df.options); + } + }); } patch_refresh_and_load_lib() { @@ -611,7 +618,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { args: { doctype: this.doctype, filters: this.get_filters_for_args(), - fields: [`count(distinct ${frappe.model.get_full_column_name('name', this.doctype)}) as total_count`], + fields: [`count(${frappe.model.get_full_column_name('name', this.doctype)}) as total_count`], } }).then(r => { this.total_count = r.message.values[0][0] || current_count; @@ -870,7 +877,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } return return_value; }); - + this.toggle_result_area(); this.render(); }); }); @@ -1008,13 +1015,6 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { standard: true }); - // add to desktop - items.push({ - label: __('Add to Desktop'), - action: () => frappe.add_to_desktop(doctype, doctype), - standard: true - }); - if (frappe.user.has_role('System Manager') && frappe.boot.developer_mode === 1) { // edit doctype items.push({ diff --git a/frappe/public/js/frappe/misc/user.js b/frappe/public/js/frappe/misc/user.js index 4b6b52bea9..15e91a82c5 100644 --- a/frappe/public/js/frappe/misc/user.js +++ b/frappe/public/js/frappe/misc/user.js @@ -74,7 +74,7 @@ $.extend(frappe.user, { }, get_desktop_items: function() { // hide based on permission - var modules_list = $.map(frappe.boot.desktop_icons, function(icon) { + var modules_list = $.map(frappe.boot.allowed_modules, function(icon) { var m = icon.module_name; var type = frappe.modules[m] && frappe.modules[m].type; @@ -105,14 +105,6 @@ $.extend(frappe.user, { return modules_list; }, - is_module: function(m) { - var icons = frappe.get_desktop_icons(); - for(var i=0; i").text(txt || "").html(); }, + + html2text: function(html) { + let d = document.createElement('div'); + d.innerHTML = html; + return d.textContent; + }, + is_url: function(txt) { return txt.toLowerCase().substr(0,7)=='http://' || txt.toLowerCase().substr(0,8)=='https://' @@ -665,13 +672,15 @@ Object.assign(frappe.utils, { return `${route[0]} ${route[1]}`; } }, - report_total_accumulator: function(column, values, type) { - if (column.fieldtype == "Percent" || type === "mean") { - return values.reduce((a, b) => ({content: a.content + flt(b.content)})).content / values.length; - } else if (frappe.model.is_numeric_field(column.fieldtype)) { - return values.reduce((a, b) => ({content: a.content + flt(b.content)})).content; + report_column_total: function(values, column, type) { + if (column.column.fieldtype == "Percent" || type === "mean") { + return values.reduce((a, b) => a + flt(b)) / values.length; + } else if (column.column.fieldtype == "Int") { + return values.reduce((a, b) => a + cint(b)); + } else if (frappe.model.is_numeric_field(column.column.fieldtype)) { + return values.reduce((a, b) => a + flt(b)); } else { - return false; + return null; } } }); diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index ed9b6fbaa2..1b651483a7 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -87,10 +87,8 @@ $.extend(frappe.model, { var doctype = doc.doctype; var docfields = frappe.meta.docfield_list[doctype] || []; var updated = []; - for(var fid=0;fid { - return perm.doc === docname && (perm.applicable_for === doc.doctype || !perm.applicable_for); - }); - } + && allowed_records.length); // don't set defaults for "User" link field using User Permissions! if (df.fieldtype==="Link" && df.options!=="User") { // 1 - look in user permissions for document_type=="Setup". // We don't want to include permissions of transactions to be used for defaults. if (df.linked_document_type==="Setup" - && has_user_permissions && user_permissions[df.options].length===1) { - return user_permissions[df.options][0].doc; + && has_user_permissions && allowed_records.length===1) { + return allowed_records[0]; } if(!df.ignore_user_permissions) { @@ -165,7 +161,7 @@ $.extend(frappe.model, { } var is_allowed_user_default = user_default && - (!has_user_permissions || is_doc_allowed(df.options, user_default)); + (!has_user_permissions || allowed_records.includes(user_default)); // is this user default also allowed as per user permissions? if (is_allowed_user_default) { @@ -190,7 +186,7 @@ $.extend(frappe.model, { } else if (df["default"][0]===":") { var boot_doc = frappe.model.get_default_from_boot_docs(df, doc, parent_doc); - var is_allowed_boot_doc = !has_user_permissions || is_doc_allowed(df.options, boot_doc); + var is_allowed_boot_doc = !has_user_permissions || allowed_records.includes(boot_doc); if (is_allowed_boot_doc) { return boot_doc; @@ -201,7 +197,7 @@ $.extend(frappe.model, { } // is this default value is also allowed as per user permissions? - var is_allowed_default = !has_user_permissions || is_doc_allowed(df.options, df.default); + var is_allowed_default = !has_user_permissions || allowed_records.includes(df.default); if (df.fieldtype!=="Link" || df.options==="User" || is_allowed_default) { return df["default"]; } @@ -342,5 +338,3 @@ frappe.new_doc = function (doctype, opts, init_callback) { }); } - - diff --git a/frappe/public/js/frappe/model/perm.js b/frappe/public/js/frappe/model/perm.js index 60ae89a3da..2396962b94 100644 --- a/frappe/public/js/frappe/model/perm.js +++ b/frappe/public/js/frappe/model/perm.js @@ -151,18 +151,10 @@ $.extend(frappe.perm, { let rules = {}; let fields_to_check = frappe.meta.get_fields_to_check_permissions(doctype); $.each(fields_to_check, (i, df) => { - const user_permissions_for_doctype = user_permissions[df.options]; - // check if there are any user permission applicable for parent doctype - const has_user_permission = user_permissions_for_doctype ? user_permissions_for_doctype - .some(perm => !perm.applicable_for || perm.applicable_for === doctype) : false; - - if (has_user_permission) { - rules[df.label] = []; - user_permissions_for_doctype.map(permission => { - if (!permission.applicable_for || permission.applicable_for === doctype) { - rules[df.label].push(permission.doc); - } - }); + const user_permissions_for_doctype = user_permissions[df.options] || []; + const allowed_records = frappe.perm.get_allowed_docs_for_doctype(user_permissions_for_doctype, doctype); + if (allowed_records.length) { + rules[df.label] = allowed_records; } }); if (!$.isEmptyObject(rules)) { @@ -260,4 +252,10 @@ $.extend(frappe.perm, { return status === "None" ? false : true; }, + + get_allowed_docs_for_doctype: (user_permissions, doctype) => { + return (user_permissions || []).filter(perm => { + return (perm.applicable_for === doctype || !perm.applicable_for); + }).map(perm => perm.doc); + } }); diff --git a/frappe/public/js/frappe/recorder/RecorderDetail.vue b/frappe/public/js/frappe/recorder/RecorderDetail.vue new file mode 100644 index 0000000000..1836292a4f --- /dev/null +++ b/frappe/public/js/frappe/recorder/RecorderDetail.vue @@ -0,0 +1,251 @@ + + + + diff --git a/frappe/public/js/frappe/recorder/RecorderRoot.vue b/frappe/public/js/frappe/recorder/RecorderRoot.vue new file mode 100644 index 0000000000..142fdc35e0 --- /dev/null +++ b/frappe/public/js/frappe/recorder/RecorderRoot.vue @@ -0,0 +1,11 @@ + + + diff --git a/frappe/public/js/frappe/recorder/RequestDetail.vue b/frappe/public/js/frappe/recorder/RequestDetail.vue new file mode 100644 index 0000000000..82997cbce2 --- /dev/null +++ b/frappe/public/js/frappe/recorder/RequestDetail.vue @@ -0,0 +1,284 @@ + + + diff --git a/frappe/public/js/frappe/recorder/recorder.js b/frappe/public/js/frappe/recorder/recorder.js new file mode 100644 index 0000000000..c5c6734aa6 --- /dev/null +++ b/frappe/public/js/frappe/recorder/recorder.js @@ -0,0 +1,39 @@ +import Vue from 'vue/dist/vue.js'; +import VueRouter from 'vue-router/dist/vue-router.js'; + +import RecorderRoot from "./RecorderRoot.vue"; + +import RecorderDetail from "./RecorderDetail.vue"; +import RequestDetail from "./RequestDetail.vue"; + +Vue.use(VueRouter); +const routes = [ + { + name: "recorder-detail", + path: '/desk', + component: RecorderDetail, + }, + { + name: "request-detail", + path: '/request/:id', + component: RequestDetail, + }, +]; + +const router = new VueRouter({ + mode: 'history', + base: "/desk#recorder/", + routes: routes, +}); + +new Vue({ + el: ".recorder-container", + router: router, + data: { + page: cur_page.page.page + }, + template: "", + components: { + RecorderRoot, + } +}); diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index 031ab6f0c1..709b6881a2 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -75,6 +75,12 @@ frappe.call = function(opts) { } } + let url = opts.url; + if (!url) { + url = '/api/method/' + args.cmd; + delete args.cmd; + } + return frappe.request.call({ type: opts.type || "POST", args: args, @@ -87,7 +93,7 @@ frappe.call = function(opts) { headers: opts.headers || {}, // show_spinner: !opts.no_spinner, async: opts.async, - url: opts.url || frappe.request.url, + url, }); } @@ -376,9 +382,12 @@ frappe.request.report_error = function(xhr, request_opts) { var data = JSON.parse(xhr.responseText); if (data.exc) { var exc = (JSON.parse(data.exc) || []).join("\n"); + var locals = (JSON.parse(data.locals) || []).join("\n"); delete data.exc; + delete data.locals; } else { var exc = ""; + locals = ""; } if (exc) { @@ -410,6 +419,9 @@ frappe.request.report_error = function(xhr, request_opts) { '
    Error Report
    ', '
    ' + exc + '
    ', '
    ', + '
    Locals
    ', + '
    ' + locals + '
    ', + '
    ', '
    Request Data
    ', '
    ' + JSON.stringify(request_opts, null, "\t") + '
    ', '
    ', diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index fb15f6b561..4a4c42852a 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -84,8 +84,15 @@ frappe.get_route = function(route) { // for app route = frappe.get_raw_route_str(route).split('/'); route = $.map(route, frappe._decode_str); - var parts = route[route.length - 1].split("?"); - route[route.length - 1] = parts[0]; + var parts = null; + var doc_name = route[route.length - 1]; + // if the last part contains ? then check if it is valid query string + if(doc_name.indexOf("?") < doc_name.indexOf("=")){ + parts = doc_name.split("?"); + route[route.length - 1] = parts[0]; + } else { + parts = doc_name; + } if (parts.length > 1) { var query_params = frappe.utils.get_query_params(parts[1]); frappe.route_options = $.extend(frappe.route_options || {}, query_params); diff --git a/frappe/public/js/frappe/toolbar.js b/frappe/public/js/frappe/toolbar.js index e698563e9c..ce208d566a 100755 --- a/frappe/public/js/frappe/toolbar.js +++ b/frappe/public/js/frappe/toolbar.js @@ -21,7 +21,7 @@ $(document).on("toolbar_setup", function() { if(limits.space || limits.users || limits.expiry || limits.emails) { help_links = []; - help_links.push('
  • ' + frappe._('Usage Info') + '
  • '); + help_links.push(''); help_links.push('
  • '); $(help_links.join("\n")).insertBefore($("#toolbar-user").find("li:first")); } diff --git a/frappe/public/js/frappe/ui/app_icon.js b/frappe/public/js/frappe/ui/app_icon.js index 5ca6adb556..a2efb08aa4 100644 --- a/frappe/public/js/frappe/ui/app_icon.js +++ b/frappe/public/js/frappe/ui/app_icon.js @@ -35,7 +35,6 @@ frappe.ui.app_icon = { } }); icon = ''+ icon+''; - return icon; } else { icon = ''; } diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js index 81b0c77a50..0a08c5f0ec 100644 --- a/frappe/public/js/frappe/ui/dialog.js +++ b/frappe/public/js/frappe/ui/dialog.js @@ -85,6 +85,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { frappe.ui.open_dialogs.push(me); me.focus_on_first_input(); me.on_page_show && me.on_page_show(); + $(document).trigger('frappe.ui.Dialog:shown'); }) .on('scroll', function() { var $input = $('input:focus'); @@ -152,6 +153,10 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { this.$wrapper.removeClass('fade'); } this.$wrapper.modal("show"); + + // clear any message + this.clear_message(); + this.primary_action_fulfilled = false; this.is_visible = true; return this; diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index e43d4d61b9..54e35a6581 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -117,10 +117,14 @@ frappe.ui.Filter = class { } update_filter_tag() { - return this._filter_value_set.then(() => { - !this.$filter_tag ? this.make_tag() : this.set_filter_button_text(); - this.filter_edit_area.hide(); - }); + if (this._filter_value_set) { + return this._filter_value_set.then(() => { + !this.$filter_tag ? this.make_tag() : this.set_filter_button_text(); + this.filter_edit_area.hide(); + }); + } else { + return Promise.resolve(); + } } remove() { diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index dee44f4340..6e60fc2665 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -100,12 +100,7 @@ frappe.msgprint = function(msg, title) { if(data.message instanceof Array) { data.message.forEach(function(m) { - const msg = { - message: m, - indicator: data.indicator, - title: data.title - } - frappe.msgprint(msg); + frappe.msgprint(m); }); return; } diff --git a/frappe/public/js/frappe/ui/toolbar/modules_select.js b/frappe/public/js/frappe/ui/toolbar/modules_select.js deleted file mode 100644 index 1b41141c9b..0000000000 --- a/frappe/public/js/frappe/ui/toolbar/modules_select.js +++ /dev/null @@ -1,108 +0,0 @@ -frappe.provide('frappe.ui.toolbar'); - -frappe.ui.toolbar.ModulesSelect = class { - constructor() { - this.user = frappe.boot.user.name; - this.setup(); - } - - setup() { - this.dialog = new frappe.ui.Dialog({ - title: __('Set Desktop Icons'), - fields: [ - { - label: __('Setup for'), - fieldname: 'setup_for', - fieldtype: 'Select', - options: [ - {label: __('User'), value: 'user'}, - {label: __('Everyone'), value: 'everyone'} - ], - default: 'user', - onchange: () => { - let field = this.$setup_for; - if(field.get_value() === 'everyone') { - this.$user.$wrapper.hide(); - this.user = undefined; - field.set_description(__('Limit icon choices for all users.')); - } else { - this.$user.$wrapper.show(); - this.user = this.$user.get_value(); - field.set_description(''); - } - this.$icons_list.refresh(); - } - }, - { fieldtype: 'Column Break' }, - { - label: __('User'), - fieldname: 'user', - fieldtype: 'Link', - options: 'User', - default: frappe.boot.user.name, - onchange: () => { - this.user = this.get_value() || frappe.boot.user.name; - this.$icons_list.refresh(); - } - }, - { fieldtype: 'Section Break' }, - { - // label: __('Icons'), - fieldname: 'icons', - fieldtype: 'MultiCheck', - select_all: 1, - columns: 2, - get_data: () => { - return new Promise((resolve) => { - frappe.call({ - method: 'frappe.desk.doctype.desktop_icon.desktop_icon.get_module_icons', - args: {user: this.user}, - freeze: true, - callback: (r) => { - const icons = r.message.icons; - const user = r.message.user; - resolve(icons - .map(icon => { - const uncheck = user ? icon.hidden : icon.blocked; - return { label: icon.label, value: icon.module_name, checked:!uncheck }; - }).sort(function(a, b){ - if(a.label < b.label) return -1; - if(a.label > b.label) return 1; - return 0; - }) - ); - } - }); - }); - } - } - ] - }); - - this.dialog.set_primary_action(__('Save'), () => { - frappe.call({ - method: 'frappe.desk.doctype.desktop_icon.desktop_icon.update_icons', - args: { - hidden_list: this.$icons_list.get_unchecked_options(), - user: this.user - }, - freeze: true, - callback: () => { - window.location.href = ''; - } - }); - }); - - this.$icons_list = this.dialog.fields_dict.icons; - this.$setup_for = this.dialog.fields_dict.setup_for; - this.$user = this.dialog.fields_dict.user; - } - - show(user) { - if(user) { - this.user = user || frappe.boot.user.name; - this.$icons_list.refresh(); - } - this.dialog.show(); - } -}; \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/toolbar/navbar.html b/frappe/public/js/frappe/ui/toolbar/navbar.html index 12e4c5beaa..ea6691a7a3 100644 --- a/frappe/public/js/frappe/ui/toolbar/navbar.html +++ b/frappe/public/js/frappe/ui/toolbar/navbar.html @@ -4,7 +4,9 @@ - + @@ -22,8 +24,6 @@ {%= frappe.user.full_name() %}