Merge branch 'develop' of git://github.com/frappe/frappe into social-improvements

This commit is contained in:
Suraj Shetty 2019-03-05 22:22:35 +05:30
commit deacd63991
310 changed files with 12759 additions and 8685 deletions

View file

@ -19,10 +19,6 @@
"error",
"1tbs"
],
"space-before-function-paren": [
"error",
"never"
],
"space-unary-ops": [
"error",
{ "words": true }
@ -121,6 +117,7 @@
"md5": true,
"$": true,
"jQuery": true,
"Vue": true,
"moment": true,
"hljs": true,
"Awesomplete": true,

9
.snyk Normal file
View file

@ -0,0 +1,9 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.13.3
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
'npm:mem:20180117':
- showdown > yargs > os-locale > mem:
reason: None given
expires: '2019-04-01T10:08:52.588Z'
patch: {}

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

@ -19,15 +19,15 @@ context('Relative Timeframe', () => {
cy.get('select.condition.form-control').select("Previous");
cy.get('.filter-field select.input-with-feedback.form-control').select("1 week");
cy.server();
cy.route({
method: 'POST',
url: '/'
}).as('applyFilter');
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
cy.get('.filter-box .btn:contains("Apply")').click();
cy.wait('@applyFilter');
cy.wait('@list_refresh');
cy.get('.list-row-container').its('length').should('eq', 1);
cy.get('.list-row-container').should('contain', 'this is second todo');
cy.route('POST', '/api/method/frappe.model.utils.user_settings.save')
.as('save_user_settings');
cy.get('.remove-filter.btn').click();
cy.wait('@save_user_settings');
});
it('set relative filter for Next and check list', () => {
cy.visit('/desk#List/ToDo/List');
@ -37,14 +37,14 @@ context('Relative Timeframe', () => {
cy.get('select.condition.form-control').select("Next");
cy.get('.filter-field select.input-with-feedback.form-control').select("1 week");
cy.server();
cy.route({
method: 'POST',
url: '/'
}).as('applyFilter');
cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
cy.get('.filter-box .btn:contains("Apply")').click();
cy.wait('@applyFilter');
cy.wait('@list_refresh');
cy.get('.list-row-container').its('length').should('eq', 1);
cy.get('.list-row').should('contain', 'this is first todo');
cy.route('POST', '/api/method/frappe.model.utils.user_settings.save')
.as('save_user_settings');
cy.get('.remove-filter.btn').click();
cy.wait('@save_user_settings');
});
});

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

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.4'
__version__ = '11.1.13'
__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

@ -0,0 +1,16 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Assignment Rule', {
refresh: function(frm) {
// refresh description
frm.events.rule(frm);
},
rule: function(frm) {
if (frm.doc.rule === 'Round Robin') {
frm.get_field('rule').set_description(__('Assign one by one, in sequence'));
} else {
frm.get_field('rule').set_description(__('Assign to the one who has the least assignments'));
}
}
});

View file

@ -0,0 +1,488 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 1,
"autoname": "Prompt",
"beta": 0,
"creation": "2019-02-28 17:12:18.815830",
"custom": 0,
"description": "Automatically Assign Documents to Users",
"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,
"fieldname": "document_type",
"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": "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": 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,
"description": "Higher priority rule will be applied first",
"fieldname": "priority",
"fieldtype": "Int",
"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": "Priority",
"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": "disabled",
"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": "Disabled",
"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_4",
"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,
"default": "Automatic Assignment",
"description": "Example: {{ subject }}",
"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": 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": "assignment_rules_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Assignment Rules",
"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,
"description": "Simple Python Expression, Example: status == 'Open' and type == 'Bug'",
"fieldname": "assign_condition",
"fieldtype": "Small Text",
"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": "Assign Condition",
"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": 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": "column_break_6",
"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,
"description": "Simple Python Expression, Example: Status in (\"Closed\", \"Cancelled\")",
"fieldname": "unassign_condition",
"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": "Unassign Condition",
"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": "assign_to_users_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Assign To Users",
"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": "rule",
"fieldtype": "Select",
"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": "Rule",
"length": 0,
"no_copy": 0,
"options": "Round Robin\nLoad Balancing",
"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": "users",
"fieldtype": "Table",
"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": "Users",
"length": 0,
"no_copy": 0,
"options": "Assignment Rule User",
"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": "last_user",
"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": "Last User",
"length": 0,
"no_copy": 0,
"options": "User",
"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
}
],
"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-28 17:12:44.413782",
"modified_by": "Administrator",
"module": "Automation",
"name": "Assignment Rule",
"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
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View file

@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.desk.form import assign_to
class AssignmentRule(Document):
def on_update(self): # pylint: disable=no-self-use
frappe.cache().delete_value('assignment_rule')
def after_rename(self): # pylint: disable=no-self-use
frappe.cache().delete_value('assignment_rule')
def apply(self, doc):
assignments = assign_to.get(doc)
if not assignments and self.safe_eval('assign_condition', doc):
self.do_assignment(doc)
return True
# try clearing
if assignments and self.unassign_condition:
return self.clear_assignment(doc)
return False
def do_assignment(self, doc):
# clear existing assignment, to reassign
assign_to.clear(doc.get('doctype'), doc.get('name'))
user = self.get_user()
assign_to.add(dict(
assign_to = user,
doctype = doc.get('doctype'),
name = doc.get('name'),
description = frappe.render_template(self.description, doc)
))
# set for reference in round robin
self.db_set('last_user', user)
def clear_assignment(self, doc):
'''Clear assignments'''
if self.safe_eval('unassign_condition', doc):
return assign_to.clear(doc.get('doctype'), doc.get('name'))
def get_user(self):
'''
Get the next user for assignment
'''
if self.rule == 'Round Robin':
return self.get_user_round_robin()
elif self.rule == 'Load Balancing':
return self.get_user_load_balancing()
def get_user_round_robin(self):
'''
Get next user based on round robin
'''
# first time, or last in list, pick the first
if not self.last_user or self.last_user == self.users[-1].user:
return self.users[0].user
# find out the next user in the list
for i, d in enumerate(self.users):
if self.last_user == d.user:
return self.users[i+1].user
# bad last user, assign to the first one
return self.users[0].user
def get_user_load_balancing(self):
'''Assign to the user with least number of open assignments'''
counts = []
for d in self.users:
counts.append(dict(
user = d.user,
count = frappe.db.count('ToDo', dict(
reference_type = self.document_type,
owner = d.user,
status = "Open"))
))
# sort by dict value
sorted_counts = sorted(counts, key = lambda k: k['count'])
# pick the first user
return sorted_counts[0].get('user')
def safe_eval(self, fieldname, doc):
try:
return frappe.safe_eval(self.get(fieldname), None, doc)
except Exception:
# when assignment fails, don't block the document as it may be
# a part of the email pulling
frappe.msgprint(frappe._('Auto assignment failed'), indicator = 'orange')
def apply(doc, method):
if frappe.flags.in_patch or frappe.flags.in_install:
return
assignment_rules = frappe.cache().get_value('assignment_rule', get_assignment_rules)
if doc.doctype in assignment_rules:
# multiple auto assigns
for d in frappe.db.get_all('Assignment Rule', dict(document_type=doc.doctype, disabled = 0), order_by = 'priority desc'):
if frappe.get_doc('Assignment Rule', d.name).apply(doc.as_dict()):
break
def get_assignment_rules():
return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))]

View file

@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import random_string
class TestAutoAssign(unittest.TestCase):
def setUp(self):
self.assignment_rule = get_assignment_rule()
clear_assignments()
def test_round_robin(self):
note = make_note(dict(public=1))
# check if auto assigned to first user
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test@example.com')
note = make_note(dict(public=1))
# check if auto assigned to second user
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test1@example.com')
clear_assignments()
note = make_note(dict(public=1))
# check if auto assigned to third user, even if
# previous assignments where closed
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test2@example.com')
# check loop back to first user
note = make_note(dict(public=1))
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test@example.com')
def test_load_balancing(self):
self.assignment_rule.rule = 'Load Balancing'
self.assignment_rule.save()
for _ in range(30):
note = make_note(dict(public=1))
# check if each user has 10 assignments (?)
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10)
# clear 5 assignments for first user
# can't do a limit in "delete" since postgres does not support it
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', owner = 'test@example.com'), limit=5):
frappe.db.sql("delete from tabToDo where name = %s", d.name)
# add 5 more assignments
for i in range(5):
make_note(dict(public=1))
# check if each user still has 10 assignments
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10)
def test_assign_condition(self):
# check condition
note = make_note(dict(public=0))
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), None)
def test_clear_assignment(self):
note = make_note(dict(public=1))
# check if auto assigned to first user
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test@example.com')
# test auto unassign
note.public = 0
note.save()
# check if cleared
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), None)
def check_multiple_rules(self):
note = make_note(dict(public=1, notify_on_login=1))
# check if auto assigned to test3 (2nd rule is applied, as it has higher priority)
self.assertEqual(frappe.db.get_value('ToDo', dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
), 'owner'), 'test@example.com')
def clear_assignments():
frappe.db.sql("delete from tabToDo where reference_type = 'Note'")
def get_assignment_rule():
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 1')
assignment_rule = frappe.get_doc(dict(
name = 'For Note 1',
doctype = 'Assignment Rule',
priority = 0,
document_type = 'Note',
assign_condition = 'public == 1',
unassign_condition = 'pubic == 0 or notify_on_login == 1',
rule = 'Round Robin',
users = [
dict(user = 'test@example.com'),
dict(user = 'test1@example.com'),
dict(user = 'test2@example.com'),
]
)).insert()
frappe.delete_doc_if_exists('Assignment Rule', 'For Note 2')
# 2nd rule
frappe.get_doc(dict(
name = 'For Note 2',
doctype = 'Assignment Rule',
priority = 1,
document_type = 'Note',
assign_condition = 'notify_on_login == 1',
unassign_condition = 'notify_on_login == 0',
rule = 'Round Robin',
users = [
dict(user = 'test3@example.com')
]
)).insert()
return assignment_rule
def make_note(values=None):
note = frappe.get_doc(dict(
doctype = 'Note',
title = random_string(10),
content = random_string(20)
))
if values:
note.update(values)
note.insert()
return note

View file

@ -0,0 +1,76 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2019-02-27 11:41:46.602400",
"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,
"fieldname": "user",
"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": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"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
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2019-02-27 17:16:41.399261",
"modified_by": "Administrator",
"module": "Automation",
"name": "Assignment Rule User",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View file

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

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

@ -2,19 +2,16 @@
# MIT License. See license.txt
from __future__ import unicode_literals, print_function
from frappe.utils.minify import JavascriptMinify
import warnings
from six import iteritems, text_type
import subprocess
import os, frappe, json, shutil, re, warnings
from os.path import exists as path_exists, join as join_path, abspath, isdir
from distutils.spawn import find_executable
from six import iteritems, text_type
from frappe.utils.minify import JavascriptMinify
"""
Build the `public` folders and setup languages
"""
import os, frappe, json, shutil, re
app_paths = None
def setup():
global app_paths
@ -45,7 +42,7 @@ def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False)
if app:
command += ' --app {app}'.format(app=app)
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], '..'))
frappe_app_path = abspath(join_path(app_paths[0], '..'))
check_yarn()
frappe.commands.popen(command, cwd=frappe_app_path)
@ -55,7 +52,7 @@ def watch(no_compress):
pacman = get_node_pacman()
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], '..'))
frappe_app_path = abspath(join_path(app_paths[0], '..'))
check_yarn()
frappe_app_path = frappe.get_app_path('frappe', '..')
frappe.commands.popen('{pacman} run watch'.format(pacman=pacman), cwd = frappe_app_path)
@ -69,51 +66,53 @@ def check_yarn():
def make_asset_dirs(make_copy=False, restore=False):
# don't even think of making assets_path absolute - rm -rf ahead.
assets_path = os.path.join(frappe.local.sites_path, "assets")
assets_path = join_path(frappe.local.sites_path, "assets")
for dir_path in [
os.path.join(assets_path, 'js'),
os.path.join(assets_path, 'css')]:
join_path(assets_path, 'js'),
join_path(assets_path, 'css')]:
if not os.path.exists(dir_path):
if not path_exists(dir_path):
os.makedirs(dir_path)
for app_name in frappe.get_all_apps(True):
pymodule = frappe.get_module(app_name)
app_base_path = os.path.abspath(os.path.dirname(pymodule.__file__))
app_base_path = abspath(os.path.dirname(pymodule.__file__))
symlinks = []
app_public_path = join_path(app_base_path, 'public')
# app/public > assets/app
symlinks.append([os.path.join(app_base_path, 'public'), os.path.join(assets_path, app_name)])
symlinks.append([app_public_path, join_path(assets_path, app_name)])
# app/node_modules > assets/app/node_modules
symlinks.append([os.path.join(app_base_path, '..', 'node_modules'), os.path.join(assets_path, app_name, 'node_modules')])
if path_exists(abspath(app_public_path)):
symlinks.append([join_path(app_base_path, '..', 'node_modules'), join_path(assets_path, app_name, 'node_modules')])
app_doc_path = None
if os.path.isdir(os.path.join(app_base_path, 'docs')):
app_doc_path = os.path.join(app_base_path, 'docs')
if isdir(join_path(app_base_path, 'docs')):
app_doc_path = join_path(app_base_path, 'docs')
elif os.path.isdir(os.path.join(app_base_path, 'www', 'docs')):
app_doc_path = os.path.join(app_base_path, 'www', 'docs')
elif isdir(join_path(app_base_path, 'www', 'docs')):
app_doc_path = join_path(app_base_path, 'www', 'docs')
if app_doc_path:
symlinks.append([app_doc_path, os.path.join(assets_path, app_name + '_docs')])
symlinks.append([app_doc_path, join_path(assets_path, app_name + '_docs')])
for source, target in symlinks:
source = os.path.abspath(source)
if os.path.exists(source):
source = abspath(source)
if path_exists(source):
if restore:
if os.path.exists(target):
if path_exists(target):
if os.path.islink(target):
os.unlink(target)
else:
shutil.rmtree(target)
shutil.copytree(source, target)
elif make_copy:
if os.path.exists(target):
if path_exists(target):
warnings.warn('Target {target} already exists.'.format(target = target))
else:
shutil.copytree(source, target)
else:
if os.path.exists(target):
if path_exists(target):
if os.path.islink(target):
os.unlink(target)
else:
@ -124,10 +123,10 @@ def make_asset_dirs(make_copy=False, restore=False):
pass
def build(no_compress=False, verbose=False):
assets_path = os.path.join(frappe.local.sites_path, "assets")
assets_path = join_path(frappe.local.sites_path, "assets")
for target, sources in iteritems(get_build_maps()):
pack(os.path.join(assets_path, target), sources, no_compress, verbose)
pack(join_path(assets_path, target), sources, no_compress, verbose)
def get_build_maps():
"""get all build.jsons with absolute paths"""
@ -135,8 +134,8 @@ def get_build_maps():
build_maps = {}
for app_path in app_paths:
path = os.path.join(app_path, 'public', 'build.json')
if os.path.exists(path):
path = join_path(app_path, 'public', 'build.json')
if path_exists(path):
with open(path) as f:
try:
for target, sources in iteritems(json.loads(f.read())):
@ -146,7 +145,7 @@ def get_build_maps():
if isinstance(source, list):
s = frappe.get_pymodule_path(source[0], *source[1].split("/"))
else:
s = os.path.join(app_path, source)
s = join_path(app_path, source)
source_paths.append(s)
build_maps[target] = source_paths
@ -166,7 +165,7 @@ def pack(target, sources, no_compress, verbose):
for f in sources:
suffix = None
if ':' in f: f, suffix = f.split(':')
if not os.path.exists(f) or os.path.isdir(f):
if not path_exists(f) or isdir(f):
print("did not find " + f)
continue
timestamps[f] = os.path.getmtime(f)
@ -220,7 +219,7 @@ def files_dirty():
for target, sources in iteritems(get_build_maps()):
for f in sources:
if ':' in f: f, suffix = f.split(':')
if not os.path.exists(f) or os.path.isdir(f): continue
if not path_exists(f) or isdir(f): continue
if os.path.getmtime(f) != timestamps.get(f):
print(f + ' dirty')
return True
@ -233,11 +232,11 @@ def compile_less():
return
for path in app_paths:
less_path = os.path.join(path, "public", "less")
if os.path.exists(less_path):
less_path = join_path(path, "public", "less")
if path_exists(less_path):
for fname in os.listdir(less_path):
if fname.endswith(".less") and fname != "variables.less":
fpath = os.path.join(less_path, fname)
fpath = join_path(less_path, fname)
mtime = os.path.getmtime(fpath)
if fpath in timestamps and mtime == timestamps[fpath]:
continue
@ -246,5 +245,5 @@ def compile_less():
print("compiling {0}".format(fpath))
css_path = os.path.join(path, "public", "css", fname.rsplit(".", 1)[0] + ".css")
css_path = join_path(path, "public", "css", fname.rsplit(".", 1)[0] + ".css")
os.system("lessc {0} > {1}".format(fpath, css_path))

View file

@ -39,7 +39,8 @@ def clear_global_cache():
clear_website_cache()
frappe.cache().delete_value(["app_hooks", "installed_apps",
"app_modules", "module_app", "notification_config", 'system_settings',
'scheduler_events', 'time_zone', 'webhooks', 'active_domains', 'active_modules'])
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule'])
frappe.setup_module_map()
def clear_defaults_cache(user=None):

View file

@ -141,6 +141,9 @@ def send(user, room, content, type = "Content"):
def seen(message, user = None):
authenticate(user)
has_message = frappe.db.exists('Chat Message', message)
if has_message:
mess = frappe.get_doc('Chat Message', message)
mess.add_seen(user)
@ -194,6 +197,9 @@ def mark_messages_as_seen(message_names, user):
def get(name, rooms = None, fields = None):
rooms, fields = safe_json_loads(rooms, fields)
has_message = frappe.db.exists('Chat Message', name)
if has_message:
dmess = frappe.get_doc('Chat Message', name)
data = dict(
name = dmess.name,

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

@ -358,7 +358,7 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
doc.set(docfield, _file.file_url)
doc.save()
return f.as_dict()
return _file.as_dict()
def check_parent_permission(parent, child_doctype):
if parent:

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

@ -223,15 +223,16 @@ def export_csv(context, doctype, path):
frappe.destroy()
@click.command('export-fixtures')
@click.option('--app', default=None, help='Export fixtures of a specific app')
@pass_context
def export_fixtures(context):
def export_fixtures(context, app=None):
"Export fixtures"
from frappe.utils.fixtures import export_fixtures
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
export_fixtures()
export_fixtures(app=app)
finally:
frappe.destroy()

View file

@ -0,0 +1,113 @@
from __future__ import unicode_literals
import json
from six import iteritems
import frappe
from frappe import _
from frappe.desk.moduleview import (get_data, get_onboard_items, config_exists, get_module_link_items_from_list)
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]
empty_tables_by_module = get_all_empty_tables_by_module()
home_settings = frappe.db.get_value("User", frappe.session.user, 'home_settings')
if home_settings:
home_settings = json.loads(home_settings)
for module in allowed_modules_list:
module_name = module["module_name"]
if module_name in empty_tables_by_module:
module["onboard_present"] = 1
if home_settings:
category_settings = home_settings[module.get("category")] if module.get("category") else {}
if module_name not in category_settings:
module["hidden"] = 1
else:
links = category_settings[module_name]["links"]
if links:
module["links"] = get_module_link_items_from_list(module["app"], module_name, links.split(","))
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) and module_name not in active_domains:
to_add = False
# Check if config
if is_module(m) and not config_exists(app, frappe.scrub(module_name)):
to_add = False
if "condition" in m and not m["condition"]:
to_add = False
if to_add:
m["app"] = app
active_modules_list.append(m)
return active_modules_list
def get_all_empty_tables_by_module():
results = frappe.db.sql("""
SELECT
name, module
FROM information_schema.tables as i
JOIN tabDocType as d
ON i.table_name = CONCAT('tab', d.name)
WHERE table_rows = 0;
""")
empty_tables_by_module = {}
for doctype, module in results:
if module in empty_tables_by_module:
empty_tables_by_module[module].append(doctype)
else:
empty_tables_by_module[module] = [doctype]
return empty_tables_by_module
def is_domain(module):
return module.get("category") == "Domains"
def is_module(module):
return module.get("type") == "module"

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,102 @@
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
"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",
"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",
"description": "Customize forms, custom fields, scripts and translations."
},
{
"module_name": "Integrations",
"category": "Administration",
"label": _("Integrations"),
"color": "#16a085",
"icon": "octicon octicon-globe",
"type": "module",
"description": "DropBox, Woocomerce, AWS, Shopify and GoCardless."
},
{
"module_name": 'Contacts',
"category": "Administration",
"label": _("Contacts"),
"type": 'module',
"icon": "octicon octicon-book",
"color": '#ffaedb',
"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
"condition": getattr(frappe.local.conf, 'developer_mode', 0),
"description": "Doctypes, dev tools and logs."
},
# Places
{
"module_name": 'Contacts',
"type": 'module',
"icon": "octicon octicon-book",
"color": '#FFAEDB',
"hidden": 1,
"module_name": "Website",
"category": "Places",
"label": _("Website"),
"_label": _("Website"),
"color": "#16a085",
"icon": "octicon octicon-globe",
"type": "module",
"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,150 @@ 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).")
},
{
"type": "doctype",
"name": "Assignment Rule",
"description": _("Set up rules for user assignments.")
}
]
}]
},
]
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",
@ -26,6 +28,11 @@ def get_data():
"name": "Website Slideshow",
"description": _("Embed image slideshows in website pages."),
},
{
"type": "doctype",
"name": "Website Route Meta",
"description": _("Add meta tags to your web pages"),
},
]
},
{
@ -35,6 +42,7 @@ def get_data():
"type": "doctype",
"name": "Blog Post",
"description": _("Single Post (article)."),
"onboard": 1,
},
{
"type": "doctype",
@ -56,11 +64,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 +96,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

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,184 @@
# -*- 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 = get_comments_from_parent(self)
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 on_doctype_update():
frappe.db.add_index("Comment", ["reference_doctype", "reference_name"])
frappe.db.add_index("Comment", ["link_doctype", "link_name"])
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 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.doctype == 'Comment' and 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 = get_comments_from_parent(doc)
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),
# "comment_email" for Comment and "sender" for Communication
"by": getattr(doc, 'comment_email', None) or getattr(doc, 'sender', None) or doc.owner,
"name": doc.name
})
update_comments_in_parent(doc.reference_doctype, doc.reference_name, _comments)
def get_comments_from_parent(doc):
'''
get the list of comments cached in the document record in the column
`_comments`
'''
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{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,13 +6,12 @@ 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
from frappe.utils.bot import BotReply
from frappe.utils import parse_addr
from frappe.core.doctype.comment.comment import update_comment_in_doc
from collections import Counter
@ -87,19 +86,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()
@ -111,26 +106,20 @@ class Communication(Document):
user=self.reference_name, after_commit=True)
def on_update(self):
"""Update parent status as `Open` or `Replied`."""
# add to _comment property of the doctype, so it shows up in
# comments count for the list view
update_comment_in_doc(self)
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

@ -3,7 +3,26 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class DocField(Document):
pass
def get_link_doctype(self):
'''Returns the Link doctype for the docfield (if applicable)
if fieldtype is Link: Returns "options"
if fieldtype is Table MultiSelect: Returns "options" of the Link field in the Child Table
'''
if self.fieldtype == 'Link':
return self.options
if self.fieldtype == 'Table MultiSelect':
table_doctype = self.options
link_doctype = frappe.db.get_value('DocField', {
'fieldtype': 'Link',
'parenttype': 'DocType',
'parent': table_doctype,
'in_list_view': 1
}, 'options')
return link_doctype

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

@ -139,7 +139,7 @@ class File(NestedSet):
def set_folder_size(self):
"""Set folder size if folder"""
if self.is_folder and not self.is_new():
self.file_size = frappe.utils.cint(self.get_folder_size())
self.file_size = cint(self.get_folder_size())
self.db_set('file_size', self.file_size)
for folder in self.get_ancestors():
@ -176,6 +176,9 @@ class File(NestedSet):
"""
full_path = self.get_full_path()
if full_path.startswith('http'):
return True
if not os.path.exists(full_path):
frappe.throw(_("File {0} does not exist").format(self.file_url), IOError)
@ -231,7 +234,7 @@ class File(NestedSet):
else:
try:
image, filename, extn = get_web_image(self.file_url)
except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError):
except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError, TypeError):
return
size = width, height
@ -384,6 +387,9 @@ class File(NestedSet):
elif file_path.startswith("/files/"):
file_path = get_files_path(*file_path.split("/files/", 1)[1].split("/"))
elif file_path.startswith("http"):
pass
elif not self.file_url:
frappe.throw(_("There is some problem with the file url: {0}").format(file_path))

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

@ -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,
@ -1601,6 +1601,39 @@
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "home_settings",
"fieldtype": "Code",
"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": "Home Settings",
"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,
@ -2303,7 +2336,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 5,
"modified": "2018-11-21 12:34:57.652854",
"modified": "2019-03-03 11:10:06.162540",
"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
@ -328,6 +328,12 @@ class User(Document):
and reference_doctype='User'
and (reference_name=%s or owner=%s)""", (self.name, self.name))
# unlink contact
frappe.db.sql("""update `tabContact`
set `user`=null
where `user`=%s""", (self.name))
def before_rename(self, old_name, new_name, merge=False):
self.check_demo()
frappe.clear_cache(user=old_name)

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:
@ -76,8 +79,8 @@ def get_user_permissions(user=None):
out = frappe._dict(out)
frappe.cache().hset("user_permissions", user, out)
except frappe.db.SQLError:
if frappe.db.is_table_missing():
except frappe.db.SQLError as e:
if frappe.db.is_table_missing(e):
# called from patch
pass
@ -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

@ -1,55 +0,0 @@
// Copyright (c) 2016, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('User Permission for Page and Report', {
refresh: function(frm) {
frm.disable_save();
frm.role_area.hide();
},
onload: function(frm) {
if(!frm.roles_editor) {
frm.role_area = $('<div style="min-height: 300px">')
.appendTo(frm.fields_dict.roles_html.wrapper);
frm.roles_editor = new frappe.RoleEditor(frm.role_area, frm);
}
},
page: function(frm) {
frm.trigger("get_roles");
},
report: function(frm){
frm.trigger("get_roles");
},
get_roles: function(frm) {
frm.role_area.show();
return frappe.call({
method:"get_custom_roles",
doc: frm.doc,
callback: function(r) {
refresh_field('roles');
frm.roles_editor.show();
}
});
},
update: function(frm) {
if(frm.roles_editor) {
frm.roles_editor.set_roles_in_table();
}
return frappe.call({
method:"set_custom_roles",
doc: frm.doc,
callback: function(r) {
refresh_field('roles');
frm.roles_editor.show();
frappe.msgprint(__("Successfully Updated"));
frm.reload_doc();
}
});
}
});

View file

@ -1,309 +0,0 @@
{
"allow_copy": 1,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-02-13 17:33:25.157332",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "set_role_for",
"fieldtype": "Select",
"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": "Set Role For",
"length": 0,
"no_copy": 0,
"options": "\nPage\nReport",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.set_role_for == 'Page'",
"fieldname": "page",
"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": "Page",
"length": 0,
"no_copy": 0,
"options": "Page",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.set_role_for == 'Report'",
"fieldname": "report",
"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": "Report",
"length": 0,
"no_copy": 0,
"options": "Report",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "roles_permission",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Roles Permission",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "roles_html",
"fieldtype": "HTML",
"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": "Roles Html",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "roles",
"fieldtype": "Table",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Roles",
"length": 0,
"no_copy": 0,
"options": "Has Role",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_8",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "update",
"fieldtype": "Button",
"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": "Update",
"length": 0,
"no_copy": 0,
"options": "",
"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,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 1,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-12-21 04:24:24.963988",
"modified_by": "Administrator",
"module": "Core",
"name": "User Permission for Page and Report",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "System 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",
"track_changes": 0,
"track_seen": 0
}

View file

@ -1,49 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class UserPermissionforPageandReport(Document):
def get_custom_roles(self):
args = self.get_args()
self.set('roles', [])
name = frappe.db.get_value('Custom Role', args, "name")
if name:
doc = frappe.get_doc('Custom Role', name)
else:
doctype = self.set_role_for
docname = self.page if self.set_role_for == 'Page' else self.report
doc = frappe.get_doc(doctype, docname)
self.set('roles', doc.roles)
def set_custom_roles(self):
args = self.get_args()
name = frappe.db.get_value('Custom Role', args, "name")
args.update({
'doctype': 'Custom Role',
'roles': self.roles
})
if name:
doc = frappe.get_doc("Custom Role", name)
doc.set('roles', self.roles)
doc.save()
else:
frappe.get_doc(args).insert()
def get_args(self, row=None):
name = self.page if self.set_role_for == 'Page' else self.report
check_for_field = self.set_role_for.replace(" ","_").lower()
return {
check_for_field: name
}
def update_status(self):
return frappe.render_template

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),
@ -40,8 +40,8 @@ class PostgresDatabase(Database):
'Long Text': ('text', ''),
'Code': ('text', ''),
'Text Editor': ('text', ''),
'Markdown Editor': ('longtext', ''),
'HTML Editor': ('longtext', ''),
'Markdown Editor': ('text', ''),
'HTML Editor': ('text', ''),
'Date': ('date', ''),
'Datetime': ('timestamp', None),
'Time': ('time', '6'),

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

@ -6,7 +6,6 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.desk.form.load import get_docinfo
import frappe.share
class DuplicateToDoError(frappe.ValidationError): pass
@ -16,15 +15,11 @@ def get(args=None):
if not args:
args = frappe.local.form_dict
get_docinfo(frappe.get_doc(args.get("doctype"), args.get("name")))
return frappe.db.sql("""SELECT `owner`, `description`
FROM `tabToDo`
WHERE reference_type=%(doctype)s
AND reference_name=%(name)s
AND status='Open'
ORDER BY modified DESC
LIMIT 5""", args, as_dict=True)
return frappe.get_all('ToDo', fields = ['owner', 'description'], filters = dict(
reference_type = args.get('doctype'),
reference_name = args.get('name'),
status = 'Open'
), limit = 5)
@frappe.whitelist()
def add(args=None):
@ -100,7 +95,8 @@ def add_multiple(args=None):
add(args)
def remove_from_todo_if_already_assigned(doctype, docname):
owner = frappe.db.get_value("ToDo", {"reference_type": doctype, "reference_name": docname, "status":"Open"}, "owner")
owner = frappe.db.get_value("ToDo", {"reference_type": doctype, "reference_name": docname,
"status":"Open"}, "owner")
if owner:
remove(doctype, docname, owner)
@ -125,9 +121,18 @@ def remove(doctype, name, assign_to):
return get({"doctype": doctype, "name": name})
def clear(doctype, name):
for assign_to in frappe.db.sql_list("""select owner from `tabToDo`
where reference_type=%(doctype)s and reference_name=%(name)s""", locals()):
remove(doctype, name, assign_to)
'''
Clears assignments, return False if not assigned.
'''
assignments = frappe.db.get_all('ToDo', fields=['owner'], filters =
dict(reference_type = doctype, reference_name = name))
if not assignments:
return False
for assign_to in assignments:
remove(doctype, name, assign_to.owner)
return True
def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE',
description=None, notify=0):

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,
@ -144,17 +156,13 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
`timeline_doctype`, `timeline_name`, `reference_doctype`, `reference_name`,
`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

@ -3,6 +3,7 @@
from __future__ import unicode_literals
import frappe
import json
from frappe import _
from frappe.boot import get_allowed_pages, get_allowed_reports
from frappe.desk.doctype.desktop_icon.desktop_icon import set_hidden, clear_desktop_icons_cache
@ -24,7 +25,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)
@ -42,6 +43,42 @@ def get_data(module):
# 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
def build_config_from_file(module):
@ -184,14 +221,24 @@ 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 config_exists(app, module):
try:
frappe.get_module("{app}.config.{module}".format(app=app, module=module))
return True
except ImportError:
return False
def add_setup_section(config, app, module, label, icon):
"""Add common sections to `/desk#Module/Setup`"""
@ -213,6 +260,75 @@ def get_setup_section(app, module, label, icon):
"items": section["items"]
}
def get_onboard_items(app, module):
try:
sections = get_config(app, module)
except ImportError:
return []
onboard_items = []
fallback_items = []
if not sections:
doctype_info = get_doctype_info(module)
sections = build_standard_config(module, doctype_info)
for section in sections:
for item in section["items"]:
if item.get("onboard", 0) == 1:
onboard_items.append(item)
# in case onboard is not set
fallback_items.append(item)
if len(onboard_items) > 5:
return onboard_items
return onboard_items or fallback_items
@frappe.whitelist()
def get_links(app, module):
try:
sections = get_config(app, frappe.scrub(module))
except ImportError:
return []
link_names = []
for section in sections:
for item in section["items"]:
link_names.append(item.get("label"))
print(link_names)
return link_names
@frappe.whitelist()
def get_module_link_items_from_dict(module_link_list_map):
module_link_list_map = json.loads(module_link_list_map)
module_links = {}
for module, data in module_link_list_map.items():
print(data)
module_links[module] = get_module_link_items_from_list(data["app"], module, data["links"])
return module_links
def get_module_link_items_from_list(app, module, list_of_link_names):
try:
sections = get_config(app, frappe.scrub(module))
except ImportError:
return []
links = []
for section in sections:
for item in section["items"]:
if item.get("label", "") in list_of_link_names:
links.append(item)
return links
def set_last_modified(data):
for section in data:
for item in section["items"]:

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,
@ -238,43 +242,51 @@ def export_query():
report_name = data["report_name"]
if isinstance(data.get("file_format_type"), string_types):
file_format_type = data["file_format_type"]
if isinstance(data.get("visible_idx"), string_types):
visible_idx = json.loads(data.get("visible_idx"))
else:
visible_idx = None
if file_format_type == "Excel":
data = run(report_name, filters)
data = frappe._dict(data)
columns = get_columns_dict(data.columns)
from frappe.utils.xlsxutils import make_xlsx
xlsx_data = build_xlsx_data(columns, data, visible_idx)
xlsx_file = make_xlsx(xlsx_data, "Query Report")
frappe.response['filename'] = report_name + '.xlsx'
frappe.response['filecontent'] = xlsx_file.getvalue()
frappe.response['type'] = 'binary'
def build_xlsx_data(columns, data, visible_idx):
result = [[]]
# add column headings
for idx in range(len(data.columns)):
result[0].append(columns[idx]["label"])
# build table from dict
if isinstance(data.result[0], dict):
# build table from result
for i, row in enumerate(data.result):
# only rows which are visible in the report
if row and (i in visible_idx):
row_list = []
# only pick up rows that are visible in the report
if i in visible_idx:
row_data = []
if isinstance(row, list):
row_data = row
elif isinstance(row, dict) and row:
for idx in range(len(data.columns)):
row_list.append(row.get(columns[idx]["fieldname"], row.get(columns[idx]["label"], "")))
result.append(row_list)
elif not row:
result.append([])
else:
result = result + [d for i,d in enumerate(data.result) if (i in visible_idx)]
label = columns[idx]["label"]
fieldname = columns[idx]["fieldname"]
from frappe.utils.xlsxutils import make_xlsx
xlsx_file = make_xlsx(result, "Query Report")
row_data.append(row.get(fieldname, row.get(label, "")))
frappe.response['filename'] = report_name + '.xlsx'
frappe.response['filecontent'] = xlsx_file.getvalue()
frappe.response['type'] = 'binary'
result.append(row_data)
return result
def get_report_module_dotted_path(module, report_name):

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

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