Merge branch 'develop' of https://github.com/frappe/frappe into website-bootstrap-4

This commit is contained in:
Faris Ansari 2019-02-20 19:25:25 +05:30
commit 5a96fc70de
209 changed files with 8404 additions and 5697 deletions

View file

@ -116,6 +116,7 @@
"md5": true,
"$": true,
"jQuery": true,
"Vue": true,
"moment": true,
"hljs": true,
"Awesomplete": true,

View file

@ -1 +1 @@
skips: ['B605', 'B404', 'B603', 'B607']
skips: ['E0203', 'B605', 'B404', 'B603', 'B607']

View file

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

View file

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

View file

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

View file

@ -50,3 +50,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => {
return cy.get('@input').type(value);
}
});
Cypress.Commands.add('awesomebar', (text) => {
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 });
});

1
frappe/.pylintrc Normal file
View file

@ -0,0 +1 @@
disable=access-member-before-definition

View file

@ -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,9 +919,13 @@ 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:
if no_cache:
hooks = _dict(load_app_hooks())
else:
hooks = _dict(cache().get_value("app_hooks", load_app_hooks))

View file

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

View file

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

View file

@ -78,19 +78,14 @@ def create(user, exists_ok = False, fields = None):
exists_ok, fields = safe_json_loads(exists_ok, fields)
result = frappe.db.sql("""
SELECT *
FROM `tabChat Profile`
WHERE `user` = '{user}'
""".format(user = user))
if result:
if not exists_ok:
frappe.throw(_('Chat Profile for User {0} exists.').format(user))
else:
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))
profile = get(user, fields = fields)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,11 @@
from __future__ import unicode_literals
from frappe import _
from frappe.desk.moduleview import add_setup_section
def get_data():
return [{
"label": _("Settings"),
data = [
{
"label": _("Core"),
"icon": "fa fa-wrench",
"items": [
{
@ -12,6 +15,16 @@ def get_data():
"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",
@ -19,33 +32,157 @@ def get_data():
"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",
"label": _("Print Settings"),
"description": _("Print Style, PDF Size"),
"hide_count": True
"description": _("Set default format, page size, print style etc.")
},
{
"type": "doctype",
"name": "Website Settings",
"label": _("Website Settings"),
"description": _("Landing Page, Website Theme, Brand Setup and more"),
"hide_count": True
"name": "Print Format",
"description": _("Customized HTML Templates for printing transactions.")
},
{
"type": "doctype",
"name": "S3 Backup Settings",
"label": _("S3 Backup Settings"),
"description": _("Enable / Disable Backup, Backup Frequency"),
"hide_count": True
"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": "SMS Settings",
"label": _("SMS Settings"),
"description": _("SMS Gateway URL, Message & Receiver Parameter"),
"hide_count": True
"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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on('{doctype}', {{
refresh: function(frm) {{
// refresh: function(frm) {{
}}
// }}
}});

View file

@ -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 <b>{0}</b> provided for the field <b>{1}</b> 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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
First screen after login. Array of module icons based on permission.

View file

@ -1,3 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

View file

@ -1,26 +0,0 @@
<div><input class="form-control desktop-app-search" type="text" placeholder="{%= __("Search Application") %}">
</div>
{% if (frappe.user.has_role("System Manager")) { %}
<p class="text-right"><a href="#applications" class="btn btn-sm btn-default">Install new applications</a>
</p>
{% } %}
<hr>
<p class="text-muted small">{%= __("Checked items will be shown on desktop") %}</p>
<div class="list-group all-applications-list">
{% 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; }
%}
<div class="list-group-item" data-label="{%= module.label %}" data-name="{%= module.name %}">
<div class="checkbox">
<label>
<input type="checkbox" {% if (user_desktop_items.indexOf(module.name)!==-1) { %} checked {% } %}
data-name="{%= module.name %}"
{{ module.force_show ? "disabled" : ""}}> {%= __(module.label) %}
</label>
</div>
</div>
{% } %}
</div>

View file

@ -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 = $('<div class="help-message-item hidden"></div>')
.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 =
`
<div class="circle module-remove" style="background-color:#E0E0E0; color:#212121">
<div class="circle-text">
<b>
&times
</b>
</div>
</div>
`;
$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() {};

View file

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

View file

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

View file

@ -1,8 +0,0 @@
<div><span class="indicator blue">{{ title }}</span></div>
<p>{{ description }}</p>
<div>
<a class="btn btn-sm btn-default" href="#{{ route }}">{{ action }}</a>
<span class="help-progress" title="{{ __("You have made {0} of {1}", [count, target]) }}">
<span class="help-progress-bar" style="width: {{ Math.floor(count/target*100) }}%"></span>
</span>
</div>

View file

@ -1,21 +0,0 @@
<div style="text-align: center; padding-top: calc(40px + 3%)">
<div id="icon-grid">
{% for (var i=0, l=desktop_items.length; i < l; i++) { %}
{{ frappe.render_template("desktop_module_icon", desktop_items[i]) }}
{% } %}
</div>
<div class="help-message-wrapper hidden">
<div class="help-message-container">
<a class="close pull-right" style="margin-right: -7px;">
<i class="octicon octicon-x"></i></a>
<div class="help-messages">
</div>
<a class="left-arrow octicon octicon-chevron-left">
</a>
<a class="right-arrow octicon octicon-chevron-right">
</a>
</div>
</div>
</div>
<div style="clear: both"></div>

View file

@ -1,23 +0,0 @@
<div class="container page-body">
<div class="row">
<div class="layout-main-section">
<div class="page-content desktop-list" style="margin-top: 40px;">
{% for (var i=0, l=desktop_items.length; i < l; i++) {
var module = desktop_items[i];
%}
<div class="desktop-list-item" id="module-icon-{%= module._id %}"
data-name="{%= module.name %}" data-link="{%= module.link %}"
title="{%= module._label %}">
<h4>
<i class="{{ module.icon }} text-muted"
style="font-size: 20px; margin-right: 15px;"></i>
{{ module._label }}
</h4>
<span class="open-notification global module-count-{{ module._id }}"
style="display: none;"></span>
</div>
{% } %}
</div>
</div>
</div>
</div>

View file

@ -1,11 +0,0 @@
<div class="case-wrapper"
data-name="{{ module_name }}" data-link="{{ link }}" title="{{ _label }}">
{{ app_icon }}
<div class="case-label ellipsis">
<div class="circle module-count-{{ frappe.scrub(_id) }}" data-doctype="{{ _doctype }}" style="display: none;">
<span class="circle-text"></span>
</div>
<!-- <span id="module-count-{{ _id }}" class="octicon octicon-primitive-dot circle" style="display: None"></span> -->
<span class="case-label-text">{{ _label }}</span>
</div>
</div>

View file

View file

@ -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($('<div class="recorder-container"></div>'));
}
show() {
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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')
)))
and (communication_type='Communication')
)
)'''

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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`
from
`tabCommunication`
where
communication_type in ("Communication", "Comment")
communication_type = "Communication"
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
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
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()

View file

@ -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) => `<li class="strong module-sidebar-item">
<a class="module-link" data-name="${item.module_name}" href="#${item.link}">
<i class="fa fa-chevron-right pull-right" style="display: none;"></i>
<span>${item._label}</span>
</a>
</li>`;
let get_sidebar_html = () => {
let sidebar_items_html = page.get_page_modules()
.map(get_module_sidebar_item.bind(this)).join("");
return `<ul class="module-sidebar-nav overlay-sidebar nav nav-pills nav-stacked">
${sidebar_items_html}
<li class="divider"></li>
</ul>`;
};
// 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'));
}
};

View file

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

View file

@ -1,30 +0,0 @@
<div class="module-body">
{% for (var i=0; i < data.length; i++) { var section = data[i]; %}
{% if ((i % 2)===0) { %}<div class="row module-section">{% } %}
<div class="col-sm-6 module-section-column">
<div class="h4 section-head">
{{ section.label }}
</div>
<div class="section-body">
{% for (var j=0; j < section.items.length; j++) {
var item = section.items[j];
if(item.shown) { %}
<div style="min-height: 22px; margin-top: 8px;">
<a class="module-section-link small" data-type="{{ item.type }}"
{% if(item.type==="help") { %}
data-youtube-id="{{ item.youtube_id }}"{% } %}
href="#{{ item.route }}" style="{{ item.style }}">
{{ item.label || __(item.name) }}
</a>
{% if(item.type==="doctype") { %}
<span class="open-notification global hide"
data-doctype="{{ item.doctype || item.name }}"></span>
{% } %}
</div>
{% } %}
{% } %}
</div>
</div>
{% if ((i % 2)===1 || i===data.length-1) { %}</div>{% } %}
{% } %}
</div>

View file

@ -190,19 +190,19 @@ 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:
# 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])
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)
@ -214,6 +214,10 @@ def get_prepared_report_result(report, filters, dn="", user=None):
"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,

View file

@ -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=[]):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -174,6 +174,7 @@ def get_email_queue(recipients, sender, subject, **kwargs):
if att.get('fid'):
_attachments.append(att)
elif att.get("print_format_attachment") == 1:
if not att.get('lang', None):
att['lang'] = frappe.local.lang
att['print_letterhead'] = kwargs.get('print_letterhead')
_attachments.append(att)

View file

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

View file

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

View file

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

View file

@ -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,12 +555,31 @@ 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:
self.throw_length_exceeded_error(df, max_length, value)
elif column_type in ('int', 'bigint', 'smallint'):
max_length = max_positive_value[column_type]
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)

View file

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

View file

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

View file

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

View file

@ -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,
"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)
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)
return out
def add_seen(self, user=None):

View file

@ -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,13 +103,30 @@ def sync_customizations_for_doctype(data, folder):
# sync single doctype exculding the child doctype
def sync_single_doctype(doc_type):
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]:
if d.get(doctype_fieldname) == doc_type:
d['doctype'] = custom_doctype
doc = frappe.get_doc(d)
doc.db_insert()
_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

View file

@ -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
execute:frappe.delete_doc("Page", "modules", ignore_missing=True)
frappe.patches.v11_0.set_default_letter_head_source

View file

@ -6,6 +6,7 @@ def execute():
to_remove = ['DocPerm', 'Custom DocPerm']
for doctype in to_remove:
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))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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