Merge branch 'develop' of git://github.com/frappe/frappe into social-improvements
This commit is contained in:
commit
2de678b6e8
98 changed files with 9368 additions and 8728 deletions
|
|
@ -5,7 +5,7 @@
|
||||||
"es6": true
|
"es6": true
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 6,
|
"ecmaVersion": 8,
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"extends": "eslint:recommended",
|
"extends": "eslint:recommended",
|
||||||
|
|
@ -113,11 +113,9 @@
|
||||||
"getCookie": true,
|
"getCookie": true,
|
||||||
"getCookies": true,
|
"getCookies": true,
|
||||||
"get_url_arg": true,
|
"get_url_arg": true,
|
||||||
|
|
||||||
"md5": true,
|
"md5": true,
|
||||||
"$": true,
|
"$": true,
|
||||||
"jQuery": true,
|
"jQuery": true,
|
||||||
"Vue": true,
|
|
||||||
"moment": true,
|
"moment": true,
|
||||||
"hljs": true,
|
"hljs": true,
|
||||||
"Awesomplete": true,
|
"Awesomplete": true,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ matrix:
|
||||||
exclude:
|
exclude:
|
||||||
- python: 2.7
|
- python: 2.7
|
||||||
env: DB=postgres
|
env: DB=postgres
|
||||||
- python: 3.6
|
- python: 2.7
|
||||||
env: TEST_TYPE=ui
|
env: TEST_TYPE=ui
|
||||||
|
|
||||||
install:
|
install:
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ sudo pip install -e ~/bench
|
||||||
|
|
||||||
rm $TRAVIS_BUILD_DIR/.git/shallow
|
rm $TRAVIS_BUILD_DIR/.git/shallow
|
||||||
cd ~/ && bench init frappe-bench --python $(which python) --frappe-path $TRAVIS_BUILD_DIR
|
cd ~/ && bench init frappe-bench --python $(which python) --frappe-path $TRAVIS_BUILD_DIR
|
||||||
cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/
|
if [[ $DB == 'mariadb' ]]; then
|
||||||
cp -r $TRAVIS_BUILD_DIR/test_sites/test_site_postgres ~/frappe-bench/sites/
|
cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/
|
||||||
cp -r $TRAVIS_BUILD_DIR/test_sites/test_site_ui ~/frappe-bench/sites/
|
elif [[ $TEST_TYPE == 'ui' ]]; then
|
||||||
|
cp -r $TRAVIS_BUILD_DIR/test_sites/test_site_ui ~/frappe-bench/sites/
|
||||||
|
elif [[ $DB == 'postgres' ]]; then
|
||||||
|
cp -r $TRAVIS_BUILD_DIR/test_sites/test_site_postgres ~/frappe-bench/sites/
|
||||||
|
fi
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ if [[ $DB == 'mariadb' ]]; then
|
||||||
|
|
||||||
elif [[ $TEST_TYPE == 'ui' ]]; then
|
elif [[ $TEST_TYPE == 'ui' ]]; then
|
||||||
setup_mariadb_env 'test_site_ui'
|
setup_mariadb_env 'test_site_ui'
|
||||||
bench --site test_site_ui --force restore ./apps/frappe/test_sites/test_site_ui/test_site_ui-database.sql.gz
|
bench --site test_site_ui reinstall --yes
|
||||||
bench --site test_site_ui migrate
|
bench --site test_site_ui execute frappe.utils.install.complete_setup_wizard
|
||||||
bench --site test_site_ui scheduler disable
|
bench --site test_site_ui scheduler disable
|
||||||
cd apps/frappe && yarn && yarn cypress:run
|
cd apps/frappe && yarn && yarn cypress:run
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
context('Awesome Bar', () => {
|
context('Awesome Bar', () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
|
cy.visit('/login');
|
||||||
cy.login('Administrator', 'qwe');
|
cy.login('Administrator', 'qwe');
|
||||||
cy.visit('/desk');
|
cy.visit('/desk');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
34
cypress/integration/list_view_settings.js
Normal file
34
cypress/integration/list_view_settings.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
context('List View Settings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.login('Administrator', 'qwe');
|
||||||
|
cy.visit('/desk');
|
||||||
|
});
|
||||||
|
it('Default settings', () => {
|
||||||
|
cy.visit('/desk#List/DocType/List');
|
||||||
|
cy.get('.list-count').should('contain', "20 of");
|
||||||
|
cy.get('.sidebar-stat').should('contain', "No Tags");
|
||||||
|
});
|
||||||
|
it('disable count and sidebar stats then verify', () => {
|
||||||
|
cy.visit('/desk#List/DocType/List');
|
||||||
|
cy.get('.list-count').should('contain', "20 of");
|
||||||
|
cy.get('button').contains('Menu').click();
|
||||||
|
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
|
||||||
|
cy.get('.modal-dialog').should('contain', 'Settings');
|
||||||
|
|
||||||
|
cy.get('input[data-fieldname="disable_count"]').check({force: true});
|
||||||
|
cy.get('input[data-fieldname="disable_sidebar_stats"]').check({force: true});
|
||||||
|
cy.get('button').filter(':visible').contains('Save').click();
|
||||||
|
|
||||||
|
cy.reload();
|
||||||
|
|
||||||
|
cy.get('.list-count').should('be.empty');
|
||||||
|
cy.get('.list-sidebar .sidebar-stat').should('not.exist');
|
||||||
|
|
||||||
|
cy.get('button').contains('Menu').click({force: true});
|
||||||
|
cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click();
|
||||||
|
cy.get('.modal-dialog').should('contain', 'Settings');
|
||||||
|
cy.get('input[data-fieldname="disable_count"]').uncheck({force: true});
|
||||||
|
cy.get('input[data-fieldname="disable_sidebar_stats"]').uncheck({force: true});
|
||||||
|
cy.get('button').filter(':visible').contains('Save').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -3,47 +3,49 @@ context('Table MultiSelect', () => {
|
||||||
cy.login('Administrator', 'qwe');
|
cy.login('Administrator', 'qwe');
|
||||||
});
|
});
|
||||||
|
|
||||||
let todo_description = 'table multiselect' + Math.random().toString().slice(2, 8);
|
let name = 'table multiselect' + Math.random().toString().slice(2, 8);
|
||||||
|
|
||||||
it('select value from multiselect dropdown', () => {
|
it('select value from multiselect dropdown', () => {
|
||||||
cy.visit('/desk#Form/ToDo/New ToDo 1');
|
cy.new_form('Assignment Rule');
|
||||||
cy.fill_field('description', todo_description, 'Text Editor').blur();
|
cy.fill_field('__newname', name);
|
||||||
cy.get('input[data-fieldname="assign_to"]').focus().as('input');
|
cy.fill_field('document_type', 'ToDo');
|
||||||
cy.get('input[data-fieldname="assign_to"] + ul').should('be.visible');
|
cy.fill_field('assign_condition', 'status=="Open"');
|
||||||
cy.get('@input').type('faris{enter}', { delay: 100 });
|
cy.get('input[data-fieldname="users"]').focus().as('input');
|
||||||
cy.get('.frappe-control[data-fieldname="assign_to"] .form-control .tb-selected-value')
|
cy.get('input[data-fieldname="users"] + ul').should('be.visible');
|
||||||
|
cy.get('@input').type('test{enter}', { delay: 100 });
|
||||||
|
cy.get('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value')
|
||||||
.first().as('selected-value');
|
.first().as('selected-value');
|
||||||
cy.get('@selected-value').should('contain', 'faris@erpnext.com');
|
cy.get('@selected-value').should('contain', 'test@erpnext.com');
|
||||||
|
|
||||||
cy.server();
|
cy.server();
|
||||||
cy.route('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form');
|
cy.route('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form');
|
||||||
// trigger save
|
// trigger save
|
||||||
cy.get('.primary-action').click();
|
cy.get('.primary-action').click();
|
||||||
cy.wait('@save_form').its('status').should('eq', 200);
|
cy.wait('@save_form').its('status').should('eq', 200);
|
||||||
cy.get('@selected-value').should('contain', 'faris@erpnext.com');
|
cy.get('@selected-value').should('contain', 'test@erpnext.com');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delete value using backspace', () => {
|
it('delete value using backspace', () => {
|
||||||
cy.visit('/desk#List/ToDo/List');
|
cy.go_to_list('Assignment Rule');
|
||||||
cy.get(`.list-subject:contains("table multiselect")`).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('input[data-fieldname="users"]').focus().type('{backspace}');
|
||||||
cy.get('.frappe-control[data-fieldname="assign_to"] .form-control .tb-selected-value')
|
cy.get('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value')
|
||||||
.should('not.exist');
|
.should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delete value using x', () => {
|
it('delete value using x', () => {
|
||||||
cy.visit('/desk#List/ToDo/List');
|
cy.go_to_list('Assignment Rule');
|
||||||
cy.get(`.list-subject:contains("table multiselect")`).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('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value').as('existing_value');
|
||||||
cy.get('@existing_value').find('.btn-remove').click();
|
cy.get('@existing_value').find('.btn-remove').click();
|
||||||
cy.get('@existing_value').should('not.exist');
|
cy.get('@existing_value').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('navigate to selected value', () => {
|
it('navigate to selected value', () => {
|
||||||
cy.visit('/desk#List/ToDo/List');
|
cy.go_to_list('Assignment Rule');
|
||||||
cy.get(`.list-subject:contains("table multiselect")`).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('.frappe-control[data-fieldname="users"] .form-control .tb-selected-value').as('existing_value');
|
||||||
cy.get('@existing_value').find('.btn-link-to-form').click();
|
cy.get('@existing_value').find('.btn-link-to-form').click();
|
||||||
cy.location('hash').should('contain', 'Form/User/faris@erpnext.com');
|
cy.location('hash').should('contain', 'Form/User/test@erpnext.com');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,9 @@
|
||||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
|
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
|
||||||
Cypress.Commands.add('login', (email, password) => {
|
Cypress.Commands.add('login', (email, password) => {
|
||||||
cy.request({
|
cy.request({
|
||||||
url: '/',
|
url: '/api/method/login',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
cmd: 'login',
|
|
||||||
usr: email,
|
usr: email,
|
||||||
pwd: password
|
pwd: password
|
||||||
}
|
}
|
||||||
|
|
@ -54,3 +53,11 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => {
|
||||||
Cypress.Commands.add('awesomebar', (text) => {
|
Cypress.Commands.add('awesomebar', (text) => {
|
||||||
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 });
|
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('new_form', (doctype) => {
|
||||||
|
cy.visit(`/desk#Form/${doctype}/New ${doctype} 1`);
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('go_to_list', (doctype) => {
|
||||||
|
cy.visit(`/desk#List/${doctype}/List`);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ if sys.version[0] == '2':
|
||||||
reload(sys)
|
reload(sys)
|
||||||
sys.setdefaultencoding("utf-8")
|
sys.setdefaultencoding("utf-8")
|
||||||
|
|
||||||
__version__ = '11.1.13'
|
__version__ = '11.1.14'
|
||||||
__title__ = "Frappe Framework"
|
__title__ = "Frappe Framework"
|
||||||
|
|
||||||
local = Local()
|
local = Local()
|
||||||
|
|
|
||||||
|
|
@ -380,7 +380,7 @@
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
"columns": 0,
|
"columns": 0,
|
||||||
"fieldname": "users",
|
"fieldname": "users",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table MultiSelect",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"ignore_user_permissions": 0,
|
"ignore_user_permissions": 0,
|
||||||
"ignore_xss_filter": 0,
|
"ignore_xss_filter": 0,
|
||||||
|
|
@ -449,7 +449,7 @@
|
||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 0,
|
"istable": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2019-02-28 17:12:44.413782",
|
"modified": "2019-03-08 15:13:01.379471",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Automation",
|
"module": "Automation",
|
||||||
"name": "Assignment Rule",
|
"name": "Assignment Rule",
|
||||||
|
|
@ -476,7 +476,7 @@
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 0,
|
||||||
"read_only": 0,
|
"read_only": 0,
|
||||||
"read_only_onload": 0,
|
"read_only_onload": 0,
|
||||||
"show_name_in_global_search": 0,
|
"show_name_in_global_search": 0,
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,25 @@ class AssignmentRule(Document):
|
||||||
frappe.cache().delete_value('assignment_rule')
|
frappe.cache().delete_value('assignment_rule')
|
||||||
|
|
||||||
def apply(self, doc):
|
def apply(self, doc):
|
||||||
assignments = assign_to.get(doc)
|
assignments = self.get_assignments(doc)
|
||||||
if not assignments and self.safe_eval('assign_condition', doc):
|
if not assignments and self.safe_eval('assign_condition', doc):
|
||||||
self.do_assignment(doc)
|
self.do_assignment(doc)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# try clearing
|
# try clearing
|
||||||
if assignments and self.unassign_condition:
|
if (self.unassign_condition and assignments and
|
||||||
|
self.name in [d.assignment_rule for d in assignments]):
|
||||||
return self.clear_assignment(doc)
|
return self.clear_assignment(doc)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_assignments(self, doc):
|
||||||
|
return frappe.get_all('ToDo', fields = ['name', 'assignment_rule'], filters = dict(
|
||||||
|
reference_type = doc.get('doctype'),
|
||||||
|
reference_name = doc.get('name'),
|
||||||
|
status = 'Open'
|
||||||
|
), limit = 5)
|
||||||
|
|
||||||
def do_assignment(self, doc):
|
def do_assignment(self, doc):
|
||||||
# clear existing assignment, to reassign
|
# clear existing assignment, to reassign
|
||||||
assign_to.clear(doc.get('doctype'), doc.get('name'))
|
assign_to.clear(doc.get('doctype'), doc.get('name'))
|
||||||
|
|
@ -37,7 +45,8 @@ class AssignmentRule(Document):
|
||||||
assign_to = user,
|
assign_to = user,
|
||||||
doctype = doc.get('doctype'),
|
doctype = doc.get('doctype'),
|
||||||
name = doc.get('name'),
|
name = doc.get('name'),
|
||||||
description = frappe.render_template(self.description, doc)
|
description = frappe.render_template(self.description, doc),
|
||||||
|
assignment_rule = self.name
|
||||||
))
|
))
|
||||||
|
|
||||||
# set for reference in round robin
|
# set for reference in round robin
|
||||||
|
|
@ -95,10 +104,10 @@ class AssignmentRule(Document):
|
||||||
def safe_eval(self, fieldname, doc):
|
def safe_eval(self, fieldname, doc):
|
||||||
try:
|
try:
|
||||||
return frappe.safe_eval(self.get(fieldname), None, doc)
|
return frappe.safe_eval(self.get(fieldname), None, doc)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
# when assignment fails, don't block the document as it may be
|
# when assignment fails, don't block the document as it may be
|
||||||
# a part of the email pulling
|
# a part of the email pulling
|
||||||
frappe.msgprint(frappe._('Auto assignment failed'), indicator = 'orange')
|
frappe.msgprint(frappe._('Auto assignment failed: {0}').format(str(e)), indicator = 'orange')
|
||||||
|
|
||||||
def apply(doc, method):
|
def apply(doc, method):
|
||||||
if frappe.flags.in_patch or frappe.flags.in_install:
|
if frappe.flags.in_patch or frappe.flags.in_install:
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ def get_assignment_rule():
|
||||||
priority = 0,
|
priority = 0,
|
||||||
document_type = 'Note',
|
document_type = 'Note',
|
||||||
assign_condition = 'public == 1',
|
assign_condition = 'public == 1',
|
||||||
unassign_condition = 'pubic == 0 or notify_on_login == 1',
|
unassign_condition = 'public == 0 or notify_on_login == 1',
|
||||||
rule = 'Round Robin',
|
rule = 'Round Robin',
|
||||||
users = [
|
users = [
|
||||||
dict(user = 'test@example.com'),
|
dict(user = 'test@example.com'),
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ import frappe
|
||||||
from frappe.chat.doctype.chat_message import chat_message
|
from frappe.chat.doctype.chat_message import chat_message
|
||||||
from frappe.chat.util import create_test_user
|
from frappe.chat.util import create_test_user
|
||||||
|
|
||||||
session = frappe.session
|
|
||||||
test_user = create_test_user(__name__)
|
|
||||||
|
|
||||||
class TestChatMessage(unittest.TestCase):
|
class TestChatMessage(unittest.TestCase):
|
||||||
def test_send(self):
|
def test_send(self):
|
||||||
|
|
|
||||||
|
|
@ -48,29 +48,31 @@ def authenticate(user):
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get(user, fields = None):
|
def get(user, fields = None):
|
||||||
duser = frappe.get_doc('User', user)
|
duser = frappe.get_doc('User', user)
|
||||||
dprof = frappe.get_doc('Chat Profile', user)
|
|
||||||
|
|
||||||
# If you're adding something here, make sure the client recieves it.
|
if frappe.db.exists('Chat Profile', user):
|
||||||
profile = dict(
|
dprof = frappe.get_doc('Chat Profile', user)
|
||||||
# User
|
|
||||||
name = duser.name,
|
|
||||||
email = duser.email,
|
|
||||||
first_name = duser.first_name,
|
|
||||||
last_name = duser.last_name,
|
|
||||||
username = duser.username,
|
|
||||||
avatar = duser.user_image,
|
|
||||||
bio = duser.bio,
|
|
||||||
# Chat Profile
|
|
||||||
status = dprof.status,
|
|
||||||
chat_background = dprof.chat_background,
|
|
||||||
message_preview = bool(dprof.message_preview),
|
|
||||||
notification_tones = bool(dprof.notification_tones),
|
|
||||||
conversation_tones = bool(dprof.conversation_tones),
|
|
||||||
enable_chat = bool(dprof.enable_chat)
|
|
||||||
)
|
|
||||||
profile = filter_dict(profile, fields)
|
|
||||||
|
|
||||||
return dictify(profile)
|
# If you're adding something here, make sure the client recieves it.
|
||||||
|
profile = dict(
|
||||||
|
# User
|
||||||
|
name = duser.name,
|
||||||
|
email = duser.email,
|
||||||
|
first_name = duser.first_name,
|
||||||
|
last_name = duser.last_name,
|
||||||
|
username = duser.username,
|
||||||
|
avatar = duser.user_image,
|
||||||
|
bio = duser.bio,
|
||||||
|
# Chat Profile
|
||||||
|
status = dprof.status,
|
||||||
|
chat_background = dprof.chat_background,
|
||||||
|
message_preview = bool(dprof.message_preview),
|
||||||
|
notification_tones = bool(dprof.notification_tones),
|
||||||
|
conversation_tones = bool(dprof.conversation_tones),
|
||||||
|
enable_chat = bool(dprof.enable_chat)
|
||||||
|
)
|
||||||
|
profile = filter_dict(profile, fields)
|
||||||
|
|
||||||
|
return dictify(profile)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create(user, exists_ok = False, fields = None):
|
def create(user, exists_ok = False, fields = None):
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ import frappe
|
||||||
from frappe.chat.doctype.chat_profile import chat_profile
|
from frappe.chat.doctype.chat_profile import chat_profile
|
||||||
from frappe.chat.util import get_user_doc, create_test_user
|
from frappe.chat.util import get_user_doc, create_test_user
|
||||||
|
|
||||||
session = frappe.session
|
|
||||||
test_user = create_test_user(__name__)
|
|
||||||
|
|
||||||
class TestChatProfile(unittest.TestCase):
|
class TestChatProfile(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
"columns": 0,
|
"columns": 0,
|
||||||
"fieldname": "subject",
|
"fieldname": "subject",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Text",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"ignore_user_permissions": 0,
|
"ignore_user_permissions": 0,
|
||||||
"ignore_xss_filter": 0,
|
"ignore_xss_filter": 0,
|
||||||
|
|
@ -476,7 +476,7 @@
|
||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 0,
|
"istable": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2019-02-08 09:18:33.843171",
|
"modified": "2019-03-07 18:39:37.598451",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Core",
|
"module": "Core",
|
||||||
"name": "Comment",
|
"name": "Comment",
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -66,7 +66,9 @@ class RolePermissionforPageandReport(Document):
|
||||||
|
|
||||||
def update_disable_prepared_report(self):
|
def update_disable_prepared_report(self):
|
||||||
if self.report:
|
if self.report:
|
||||||
frappe.db.set_value('Report', self.report, 'disable_prepared_report', self.disable_prepared_report)
|
# intentionally written update query in frappe.db.sql instead of frappe.db.set_value
|
||||||
|
frappe.db.sql(""" update `tabReport` set disable_prepared_report = %s
|
||||||
|
where name = %s""", (self.disable_prepared_report, self.report))
|
||||||
|
|
||||||
def get_args(self, row=None):
|
def get_args(self, row=None):
|
||||||
name = self.page if self.set_role_for == 'Page' else self.report
|
name = self.page if self.set_role_for == 'Page' else self.report
|
||||||
|
|
|
||||||
|
|
@ -1176,6 +1176,107 @@
|
||||||
"translatable": 0,
|
"translatable": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 1,
|
||||||
|
"columns": 0,
|
||||||
|
"depends_on": "",
|
||||||
|
"fieldname": "document_follow_notifications_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": "Document Follow",
|
||||||
|
"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": "document_follow_notify",
|
||||||
|
"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": "Send Notifications for documents followed by me",
|
||||||
|
"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,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"default": "Daily",
|
||||||
|
"depends_on": "eval:(doc.document_follow_notify== 1)",
|
||||||
|
"fieldname": "document_follow_frequency",
|
||||||
|
"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": "Frequency",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"options": "Hourly\nDaily\nWeekly",
|
||||||
|
"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_bulk_edit": 0,
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
|
|
@ -1226,7 +1327,7 @@
|
||||||
"in_global_search": 0,
|
"in_global_search": 0,
|
||||||
"in_list_view": 0,
|
"in_list_view": 0,
|
||||||
"in_standard_filter": 0,
|
"in_standard_filter": 0,
|
||||||
"label": "Send Notifications for Transactions I Follow",
|
"label": "Send Notifications for Email threads",
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"no_copy": 0,
|
"no_copy": 0,
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
|
|
@ -1676,7 +1777,7 @@
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
"columns": 0,
|
"columns": 0,
|
||||||
"description": "Enter default value fields (keys) and values. If you add multiple values for a field,the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields,go to \"Customize Form\".",
|
"description": "Enter default value fields (keys) and values. If you add multiple values for a field, the first one will be picked. These defaults are also used to set \"match\" permission rules. To see list of fields, go to \"Customize Form\".",
|
||||||
"fieldname": "defaults",
|
"fieldname": "defaults",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
|
|
@ -1776,7 +1877,7 @@
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
"columns": 0,
|
"columns": 0,
|
||||||
"default": "System User",
|
"default": "System User",
|
||||||
"description": "If the user has any role checked,then the user becomes a \"System User\". \"System User\" has access to the desktop",
|
"description": "If the user has any role checked, then the user becomes a \"System User\". \"System User\" has access to the desktop",
|
||||||
"fieldname": "user_type",
|
"fieldname": "user_type",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
|
|
@ -1908,7 +2009,7 @@
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
"columns": 0,
|
"columns": 0,
|
||||||
"depends_on": "eval:doc.restrict_ip && doc.restrict_ip.length",
|
"depends_on": "eval:doc.restrict_ip && doc.restrict_ip.length",
|
||||||
"description": "If enabled, user can login from any IP Address using Two Factor Auth, this can also be set for all users in System Settings",
|
"description": "If enabled, user can login from any IP Address using Two Factor Auth, this can also be set for all users in System Settings",
|
||||||
"fieldname": "bypass_restrict_ip_check_if_2fa_enabled",
|
"fieldname": "bypass_restrict_ip_check_if_2fa_enabled",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
|
|
@ -2336,7 +2437,7 @@
|
||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 0,
|
"istable": 0,
|
||||||
"max_attachments": 5,
|
"max_attachments": 5,
|
||||||
"modified": "2019-03-03 11:10:06.162540",
|
"modified": "2019-03-03 11:10:06.162541",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Core",
|
"module": "Core",
|
||||||
"name": "User",
|
"name": "User",
|
||||||
|
|
|
||||||
|
|
@ -1048,7 +1048,7 @@ def update_roles(role_profile):
|
||||||
user.set('roles', [])
|
user.set('roles', [])
|
||||||
user.add_roles(*roles)
|
user.add_roles(*roles)
|
||||||
|
|
||||||
def create_contact(user, ignore_links=False):
|
def create_contact(user, ignore_links=False, ignore_mandatory=False):
|
||||||
if user.name in ["Administrator", "Guest"]: return
|
if user.name in ["Administrator", "Guest"]: return
|
||||||
|
|
||||||
if not frappe.db.get_value("Contact", {"email_id": user.email}):
|
if not frappe.db.get_value("Contact", {"email_id": user.email}):
|
||||||
|
|
@ -1061,7 +1061,7 @@ def create_contact(user, ignore_links=False):
|
||||||
"gender": user.gender,
|
"gender": user.gender,
|
||||||
"phone": user.phone,
|
"phone": user.phone,
|
||||||
"mobile_no": user.mobile_no
|
"mobile_no": user.mobile_no
|
||||||
}).insert(ignore_permissions=True, ignore_links=ignore_links)
|
}).insert(ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -38,6 +38,7 @@ docfield_properties = {
|
||||||
'fieldtype': 'Select',
|
'fieldtype': 'Select',
|
||||||
'options': 'Text',
|
'options': 'Text',
|
||||||
'fetch_from': 'Small Text',
|
'fetch_from': 'Small Text',
|
||||||
|
'fetch_if_empty': 'Check',
|
||||||
'permlevel': 'Int',
|
'permlevel': 'Int',
|
||||||
'width': 'Data',
|
'width': 'Data',
|
||||||
'print_width': 'Data',
|
'print_width': 'Data',
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,107 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# See license.txt
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from frappe.desk.doctype.desktop_icon.desktop_icon import (get_desktop_icons, add_user_icon,
|
|
||||||
set_hidden_list, set_order, clear_desktop_icons_cache)
|
|
||||||
|
|
||||||
# test_records = frappe.get_test_records('Desktop Icon')
|
|
||||||
|
|
||||||
class TestDesktopIcon(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
frappe.set_user('test@example.com')
|
|
||||||
frappe.db.sql('delete from `tabDesktop Icon` where standard=0')
|
|
||||||
frappe.db.sql('delete from `tabBlock Module`')
|
|
||||||
frappe.db.sql('update `tabDesktop Icon` set hidden=0, blocked=0')
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
frappe.set_user('Administrator')
|
|
||||||
|
|
||||||
def get_icon(self, module_name):
|
|
||||||
for i in get_desktop_icons():
|
|
||||||
if i.module_name == module_name:
|
|
||||||
return i
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def test_get_standard_desktop_icon_for_user(self):
|
|
||||||
self.assertEqual(self.get_icon('Desk').standard, 1)
|
|
||||||
|
|
||||||
def test_add_desktop_icon(self):
|
|
||||||
self.assertEqual(self.get_icon('User'), None)
|
|
||||||
add_user_icon('User')
|
|
||||||
|
|
||||||
icon = self.get_icon('User')
|
|
||||||
self.assertEqual(icon.custom, 1)
|
|
||||||
self.assertEqual(icon.standard, 0)
|
|
||||||
|
|
||||||
def test_hide_desktop_icon(self):
|
|
||||||
set_hidden_list(["Desk"], 'test@example.com')
|
|
||||||
|
|
||||||
icon = self.get_icon('Desk')
|
|
||||||
self.assertEqual(icon.hidden, 1)
|
|
||||||
self.assertEqual(icon.standard, 0)
|
|
||||||
|
|
||||||
def test_remove_custom_desktop_icon_on_hidden(self):
|
|
||||||
self.test_add_desktop_icon()
|
|
||||||
set_hidden_list(['User'], 'test@example.com')
|
|
||||||
|
|
||||||
icon = self.get_icon('User')
|
|
||||||
self.assertEqual(icon, None)
|
|
||||||
|
|
||||||
def test_show_desktop_icon(self):
|
|
||||||
self.test_hide_desktop_icon()
|
|
||||||
set_hidden_list([], 'test@example.com')
|
|
||||||
|
|
||||||
icon = self.get_icon('Desk')
|
|
||||||
self.assertEqual(icon.hidden, 0)
|
|
||||||
self.assertEqual(icon.standard, 0)
|
|
||||||
|
|
||||||
def test_globally_hidden_desktop_icon(self):
|
|
||||||
set_hidden_list(["Desk"])
|
|
||||||
|
|
||||||
icon = self.get_icon('Desk')
|
|
||||||
self.assertEqual(icon.hidden, 1)
|
|
||||||
|
|
||||||
frappe.set_user('test1@example.com')
|
|
||||||
icon = self.get_icon('Desk')
|
|
||||||
self.assertEqual(icon.hidden, 1)
|
|
||||||
|
|
||||||
def test_re_order_desktop_icons(self):
|
|
||||||
icons = [d.module_name for d in get_desktop_icons()]
|
|
||||||
m0, m1 = icons[0], icons[1]
|
|
||||||
set_order([m1, m0] + icons[2:], frappe.session.user)
|
|
||||||
|
|
||||||
# reload
|
|
||||||
icons = [d.module_name for d in get_desktop_icons()]
|
|
||||||
|
|
||||||
# check switched order
|
|
||||||
self.assertEqual(icons[0], m1)
|
|
||||||
self.assertEqual(icons[1], m0)
|
|
||||||
|
|
||||||
def test_block_desktop_icons_for_user(self):
|
|
||||||
def test_unblock():
|
|
||||||
user = frappe.get_doc('User', 'test@example.com')
|
|
||||||
user.block_modules = []
|
|
||||||
user.save(ignore_permissions = 1)
|
|
||||||
|
|
||||||
icon = self.get_icon('Desk')
|
|
||||||
self.assertEqual(icon.hidden, 0)
|
|
||||||
|
|
||||||
test_unblock()
|
|
||||||
|
|
||||||
user = frappe.get_doc('User', 'test@example.com')
|
|
||||||
user.append('block_modules', {'module': 'Desk'})
|
|
||||||
user.save(ignore_permissions = 1)
|
|
||||||
clear_desktop_icons_cache(user.name)
|
|
||||||
|
|
||||||
icon = self.get_icon('Desk')
|
|
||||||
self.assertEqual(icon.hidden, 1)
|
|
||||||
|
|
||||||
test_unblock()
|
|
||||||
|
|
||||||
|
|
||||||
0
frappe/desk/doctype/list_view_setting/__init__.py
Normal file
0
frappe/desk/doctype/list_view_setting/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('List View Setting', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
||||||
160
frappe/desk/doctype/list_view_setting/list_view_setting.json
Normal file
160
frappe/desk/doctype/list_view_setting/list_view_setting.json
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
{
|
||||||
|
"allow_copy": 0,
|
||||||
|
"allow_events_in_timeline": 0,
|
||||||
|
"allow_guest_to_view": 0,
|
||||||
|
"allow_import": 0,
|
||||||
|
"allow_rename": 0,
|
||||||
|
"autoname": "Prompt",
|
||||||
|
"beta": 0,
|
||||||
|
"creation": "2019-03-06 13:29:21.101860",
|
||||||
|
"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": "disable_count",
|
||||||
|
"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": "Disable Count",
|
||||||
|
"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": "disable_sidebar_stats",
|
||||||
|
"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": "Disable Sidebar Stats",
|
||||||
|
"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": "disable_auto_refresh",
|
||||||
|
"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": "Disable Auto Refresh",
|
||||||
|
"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-03-06 13:40:59.533586",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Desk",
|
||||||
|
"name": "List View Setting",
|
||||||
|
"name_case": "",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"amend": 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": 0,
|
||||||
|
"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
|
||||||
|
}
|
||||||
10
frappe/desk/doctype/list_view_setting/list_view_setting.py
Normal file
10
frappe/desk/doctype/list_view_setting/list_view_setting.py
Normal 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 ListViewSetting(Document):
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestListViewSetting(unittest.TestCase):
|
||||||
|
pass
|
||||||
|
|
@ -46,3 +46,31 @@ class TestToDo(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(todo.assigned_by_full_name,
|
self.assertEqual(todo.assigned_by_full_name,
|
||||||
frappe.db.get_value('User', todo.assigned_by, 'full_name'))
|
frappe.db.get_value('User', todo.assigned_by, 'full_name'))
|
||||||
|
|
||||||
|
def test_fetch_if_empty(self):
|
||||||
|
frappe.db.sql('delete from tabToDo')
|
||||||
|
|
||||||
|
# Allow user changes
|
||||||
|
todo_meta = frappe.get_doc('DocType', 'ToDo')
|
||||||
|
field = todo_meta.get('fields', dict(fieldname='assigned_by_full_name'))[0]
|
||||||
|
field.fetch_from = 'assigned_by.full_name'
|
||||||
|
field.fetch_if_empty = 1
|
||||||
|
todo_meta.save()
|
||||||
|
|
||||||
|
frappe.clear_cache(doctype='ToDo')
|
||||||
|
|
||||||
|
todo = frappe.get_doc(dict(doctype='ToDo', description='test todo',
|
||||||
|
assigned_by='Administrator', assigned_by_full_name='Admin')).insert()
|
||||||
|
|
||||||
|
self.assertEqual(todo.assigned_by_full_name, 'Admin')
|
||||||
|
|
||||||
|
# Overwrite user changes
|
||||||
|
todo_meta = frappe.get_doc('DocType', 'ToDo')
|
||||||
|
todo_meta.get('fields', dict(fieldname='assigned_by_full_name'))[0].fetch_if_empty = 0
|
||||||
|
todo_meta.save()
|
||||||
|
|
||||||
|
todo.reload()
|
||||||
|
todo.save()
|
||||||
|
|
||||||
|
self.assertEqual(todo.assigned_by_full_name,
|
||||||
|
frappe.db.get_value('User', todo.assigned_by, 'full_name'))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"allow_copy": 0,
|
||||||
|
"allow_events_in_timeline": 0,
|
||||||
"allow_guest_to_view": 0,
|
"allow_guest_to_view": 0,
|
||||||
"allow_import": 0,
|
"allow_import": 0,
|
||||||
"allow_rename": 0,
|
"allow_rename": 0,
|
||||||
|
|
@ -15,6 +16,7 @@
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -45,6 +47,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -77,6 +80,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -111,6 +115,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -140,6 +145,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -171,6 +177,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -203,6 +210,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -234,6 +242,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -265,6 +274,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -299,6 +309,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -329,6 +340,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -362,6 +374,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -395,6 +408,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -424,6 +438,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -457,6 +472,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -488,6 +504,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -521,6 +538,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
|
|
@ -549,6 +567,39 @@
|
||||||
"set_only_once": 0,
|
"set_only_once": 0,
|
||||||
"translatable": 0,
|
"translatable": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"fieldname": "assignment_rule",
|
||||||
|
"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": "Assignment Rule",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"options": "Assignment Rule",
|
||||||
|
"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,
|
"has_web_view": 0,
|
||||||
|
|
@ -562,7 +613,7 @@
|
||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 0,
|
"istable": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2018-05-16 22:43:01.858775",
|
"modified": "2019-03-07 16:11:25.764549",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Desk",
|
"module": "Desk",
|
||||||
"name": "ToDo",
|
"name": "ToDo",
|
||||||
|
|
@ -615,5 +666,6 @@
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"title_field": "description",
|
"title_field": "description",
|
||||||
"track_changes": 1,
|
"track_changes": 1,
|
||||||
"track_seen": 1
|
"track_seen": 1,
|
||||||
|
"track_views": 0
|
||||||
}
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.desk.form.document_follow import follow_document
|
||||||
import frappe.share
|
import frappe.share
|
||||||
|
|
||||||
class DuplicateToDoError(frappe.ValidationError): pass
|
class DuplicateToDoError(frappe.ValidationError): pass
|
||||||
|
|
@ -28,7 +29,8 @@ def add(args=None):
|
||||||
"assign_to": ,
|
"assign_to": ,
|
||||||
"doctype": ,
|
"doctype": ,
|
||||||
"name": ,
|
"name": ,
|
||||||
"description":
|
"description": ,
|
||||||
|
"assignment_rule":
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -42,7 +44,6 @@ def add(args=None):
|
||||||
AND `status`='Open'
|
AND `status`='Open'
|
||||||
AND `owner`=%(assign_to)s""", args):
|
AND `owner`=%(assign_to)s""", args):
|
||||||
frappe.throw(_("Already in user's To Do list"), DuplicateToDoError)
|
frappe.throw(_("Already in user's To Do list"), DuplicateToDoError)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
from frappe.utils import nowdate
|
from frappe.utils import nowdate
|
||||||
|
|
||||||
|
|
@ -62,6 +63,7 @@ def add(args=None):
|
||||||
"status": "Open",
|
"status": "Open",
|
||||||
"date": args.get('date', nowdate()),
|
"date": args.get('date', nowdate()),
|
||||||
"assigned_by": args.get('assigned_by', frappe.session.user),
|
"assigned_by": args.get('assigned_by', frappe.session.user),
|
||||||
|
'assignment_rule': args.get('assignment_rule')
|
||||||
}).insert(ignore_permissions=True)
|
}).insert(ignore_permissions=True)
|
||||||
|
|
||||||
# set assigned_to if field exists
|
# set assigned_to if field exists
|
||||||
|
|
@ -75,6 +77,9 @@ def add(args=None):
|
||||||
frappe.share.add(doc.doctype, doc.name, args['assign_to'])
|
frappe.share.add(doc.doctype, doc.name, args['assign_to'])
|
||||||
frappe.msgprint(_('Shared with user {0} with read access').format(args['assign_to']), alert=True)
|
frappe.msgprint(_('Shared with user {0} with read access').format(args['assign_to']), alert=True)
|
||||||
|
|
||||||
|
# make this document followed by assigned user
|
||||||
|
follow_document(args['doctype'], args['name'], args['assign_to'])
|
||||||
|
|
||||||
# notify
|
# notify
|
||||||
notify_assignment(d.assigned_by, d.owner, d.reference_type, d.reference_name, action='ASSIGN',\
|
notify_assignment(d.assigned_by, d.owner, d.reference_type, d.reference_name, action='ASSIGN',\
|
||||||
description=args.get("description"), notify=args.get('notify'))
|
description=args.get("description"), notify=args.get('notify'))
|
||||||
|
|
|
||||||
280
frappe/desk/form/document_follow.py
Normal file
280
frappe/desk/form/document_follow.py
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# MIT License. See license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
import frappe.utils
|
||||||
|
from frappe.utils import get_url_to_form
|
||||||
|
from frappe import _
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def follow_document(doctype, doc_name, user, force=False):
|
||||||
|
'''
|
||||||
|
param:
|
||||||
|
Doctype name
|
||||||
|
doc name
|
||||||
|
user email
|
||||||
|
|
||||||
|
condition:
|
||||||
|
avoided for some doctype
|
||||||
|
follow only if track changes are set to 1
|
||||||
|
'''
|
||||||
|
avoid_follow = ["Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log",
|
||||||
|
"File", "Version", "View Log", "Document Follow", "Comment"]
|
||||||
|
|
||||||
|
track_changes = frappe.get_meta(doctype).track_changes
|
||||||
|
exists = is_document_followed(doctype, doc_name, user)
|
||||||
|
if exists == 0:
|
||||||
|
user_can_follow = frappe.db.get_value("User", user, "document_follow_notify", ignore=True)
|
||||||
|
if user != "Administrator" and user_can_follow and track_changes and (doctype not in avoid_follow or force):
|
||||||
|
doc = frappe.new_doc("Document Follow")
|
||||||
|
doc.update({
|
||||||
|
"ref_doctype": doctype,
|
||||||
|
"ref_docname": doc_name,
|
||||||
|
"user": user
|
||||||
|
})
|
||||||
|
doc.save()
|
||||||
|
return doc
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def unfollow_document(doctype, doc_name, user):
|
||||||
|
doc = frappe.get_all(
|
||||||
|
"Document Follow",
|
||||||
|
filters={
|
||||||
|
"ref_doctype": doctype,
|
||||||
|
"ref_docname": doc_name,
|
||||||
|
"user": user
|
||||||
|
},
|
||||||
|
fields=["name"],
|
||||||
|
limit=1
|
||||||
|
)
|
||||||
|
if doc:
|
||||||
|
frappe.delete_doc("Document Follow", doc[0].name)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_message(doc_name, doctype, frequency):
|
||||||
|
activity_list = get_version(doctype, doc_name, frequency) + get_comments(doctype, doc_name, frequency)
|
||||||
|
return sorted(activity_list, key=lambda k: k["time"], reverse=True)
|
||||||
|
|
||||||
|
def send_email_alert(receiver, docinfo, timeline):
|
||||||
|
if receiver:
|
||||||
|
frappe.sendmail(
|
||||||
|
subject=_("Document Follow Notification"),
|
||||||
|
recipients=[receiver],
|
||||||
|
template="document_follow",
|
||||||
|
args={
|
||||||
|
"docinfo": docinfo,
|
||||||
|
"timeline": timeline,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_document_follow_mails(frequency):
|
||||||
|
|
||||||
|
'''
|
||||||
|
param:
|
||||||
|
frequency for sanding mails
|
||||||
|
|
||||||
|
task:
|
||||||
|
set receiver according to frequency
|
||||||
|
group document list according to user
|
||||||
|
get changes, activity, comments on doctype
|
||||||
|
call method to send mail
|
||||||
|
'''
|
||||||
|
|
||||||
|
users = frappe.get_list("Document Follow",
|
||||||
|
fields=["*"])
|
||||||
|
|
||||||
|
sorted_users = sorted(users, key=lambda k: k['user'])
|
||||||
|
|
||||||
|
grouped_by_user = {}
|
||||||
|
for k, v in groupby(sorted_users, key=lambda k: k['user']):
|
||||||
|
grouped_by_user[k] = list(v)
|
||||||
|
|
||||||
|
for user in grouped_by_user:
|
||||||
|
user_frequency = frappe.db.get_value("User", user, "document_follow_frequency")
|
||||||
|
message = []
|
||||||
|
valid_document_follows = []
|
||||||
|
if user_frequency == frequency:
|
||||||
|
for d in grouped_by_user[user]:
|
||||||
|
content = get_message(d.ref_docname, d.ref_doctype, frequency)
|
||||||
|
if content:
|
||||||
|
message = message + content
|
||||||
|
valid_document_follows.append({
|
||||||
|
"reference_docname": d.ref_docname,
|
||||||
|
"reference_doctype": d.ref_doctype,
|
||||||
|
"reference_url": get_url_to_form(d.ref_doctype, d.ref_docname)
|
||||||
|
})
|
||||||
|
|
||||||
|
if message:
|
||||||
|
send_email_alert(user, valid_document_follows, message)
|
||||||
|
|
||||||
|
|
||||||
|
def get_version(doctype, doc_name, frequency):
|
||||||
|
timeline = []
|
||||||
|
filters = get_filters("docname", doc_name, frequency)
|
||||||
|
version = frappe.get_all("Version",
|
||||||
|
filters=filters,
|
||||||
|
fields=["ref_doctype", "data", "modified", "modified", "modified_by"]
|
||||||
|
)
|
||||||
|
if version:
|
||||||
|
for v in version:
|
||||||
|
change = frappe.parse_json(v.data)
|
||||||
|
time = frappe.utils.format_datetime(v.modified, "hh:mm a")
|
||||||
|
timeline_items = []
|
||||||
|
if change.changed:
|
||||||
|
timeline_items = get_field_changed(change.changed, time, doctype, doc_name, v)
|
||||||
|
if change.row_changed:
|
||||||
|
timeline_items = get_row_changed(change.row_changed, time, doctype, doc_name, v)
|
||||||
|
if change.added:
|
||||||
|
timeline_items = get_added_row(change.added, time, doctype, doc_name, v)
|
||||||
|
|
||||||
|
timeline = timeline + timeline_items
|
||||||
|
|
||||||
|
return timeline
|
||||||
|
|
||||||
|
def get_comments(doctype, doc_name, frequency):
|
||||||
|
timeline = []
|
||||||
|
filters = get_filters("reference_name", doc_name, frequency)
|
||||||
|
comments = frappe.get_all("Comment",
|
||||||
|
filters=filters,
|
||||||
|
fields=["content", "modified", "modified_by", "comment_type"]
|
||||||
|
)
|
||||||
|
for comment in comments:
|
||||||
|
if comment.comment_type == "Like":
|
||||||
|
by = ''' By : <b>{0}<b>'''.format(comment.modified_by)
|
||||||
|
elif comment.comment_type == "Comment":
|
||||||
|
by = '''Commented by : <b>{0}<b>'''.format(comment.modified_by)
|
||||||
|
else:
|
||||||
|
by = ''
|
||||||
|
|
||||||
|
time = frappe.utils.format_datetime(comment.modified, "hh:mm a")
|
||||||
|
timeline.append({
|
||||||
|
"time": comment.modified,
|
||||||
|
"data": {
|
||||||
|
"time": time,
|
||||||
|
"comment": frappe.utils.html2text(str(comment.content)),
|
||||||
|
"by": by
|
||||||
|
},
|
||||||
|
"doctype": doctype,
|
||||||
|
"doc_name": doc_name,
|
||||||
|
"type": "comment"
|
||||||
|
})
|
||||||
|
return timeline
|
||||||
|
|
||||||
|
def is_document_followed(doctype, doc_name, user):
|
||||||
|
docs = frappe.get_all(
|
||||||
|
"Document Follow",
|
||||||
|
filters={
|
||||||
|
"ref_doctype": doctype,
|
||||||
|
"ref_docname": doc_name,
|
||||||
|
"user": user
|
||||||
|
},
|
||||||
|
limit=1
|
||||||
|
)
|
||||||
|
return len(docs)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_follow_users(doctype, doc_name):
|
||||||
|
return frappe.get_all(
|
||||||
|
"Document Follow",
|
||||||
|
filters={
|
||||||
|
"ref_doctype": doctype,
|
||||||
|
"ref_docname":doc_name
|
||||||
|
},
|
||||||
|
fields=["user"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_row_changed(row_changed, time, doctype, doc_name, v):
|
||||||
|
items = []
|
||||||
|
for d in row_changed:
|
||||||
|
d[2] = d[2] if d[2] else ' '
|
||||||
|
d[0] = d[0] if d[0] else ' '
|
||||||
|
d[3][0][1] = d[3][0][1] if d[3][0][1] else ' '
|
||||||
|
items.append({
|
||||||
|
"time": v.modified,
|
||||||
|
"data": {
|
||||||
|
"time": time,
|
||||||
|
"table_field": d[0],
|
||||||
|
"row": str(d[1]),
|
||||||
|
"field": d[3][0][0],
|
||||||
|
"from": frappe.utils.html2text(str(d[3][0][1])),
|
||||||
|
"to": frappe.utils.html2text(str(d[3][0][2]))
|
||||||
|
},
|
||||||
|
"doctype": doctype,
|
||||||
|
"doc_name": doc_name,
|
||||||
|
"type": "row changed",
|
||||||
|
"by": v.modified_by
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
|
||||||
|
def get_added_row(added, time, doctype, doc_name, v):
|
||||||
|
items = []
|
||||||
|
for d in added:
|
||||||
|
items.append({
|
||||||
|
"time": v.modified,
|
||||||
|
"data": {
|
||||||
|
"to": d[0],
|
||||||
|
"time": time
|
||||||
|
},
|
||||||
|
"doctype": doctype,
|
||||||
|
"doc_name": doc_name,
|
||||||
|
"type": "row added",
|
||||||
|
"by": v.modified_by
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
|
||||||
|
def get_field_changed(changed, time, doctype, doc_name, v):
|
||||||
|
items = []
|
||||||
|
for d in changed:
|
||||||
|
d[1] = d[1] if d[1] else ' '
|
||||||
|
d[2] = d[2] if d[2] else ' '
|
||||||
|
d[0] = d[0] if d[0] else ' '
|
||||||
|
items.append({
|
||||||
|
"time": v.modified,
|
||||||
|
"data": {
|
||||||
|
"time": time,
|
||||||
|
"field": d[0],
|
||||||
|
"from": frappe.utils.html2text(str(d[1])),
|
||||||
|
"to": frappe.utils.html2text(str(d[2]))
|
||||||
|
},
|
||||||
|
"doctype": doctype,
|
||||||
|
"doc_name": doc_name,
|
||||||
|
"type": "field changed",
|
||||||
|
"by": v.modified_by
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
|
||||||
|
def send_hourly_updates():
|
||||||
|
send_document_follow_mails("Hourly")
|
||||||
|
|
||||||
|
def send_daily_updates():
|
||||||
|
send_document_follow_mails("Daily")
|
||||||
|
|
||||||
|
def send_weekly_updates():
|
||||||
|
send_document_follow_mails("Weekly")
|
||||||
|
|
||||||
|
def get_filters(search_by, name, frequency):
|
||||||
|
filters = []
|
||||||
|
|
||||||
|
if frequency == "Weekly":
|
||||||
|
filters = [
|
||||||
|
[search_by, "=", name],
|
||||||
|
["modified", ">", frappe.utils.add_days(frappe.utils.nowdate(),-7)],
|
||||||
|
["modified", "<", frappe.utils.nowdate()]
|
||||||
|
]
|
||||||
|
elif frequency == "Daily":
|
||||||
|
filters = [
|
||||||
|
[search_by, "=", name],
|
||||||
|
["modified", ">", frappe.utils.add_days(frappe.utils.nowdate(),-1)],
|
||||||
|
["modified", "<", frappe.utils.nowdate()]
|
||||||
|
]
|
||||||
|
elif frequency == "Hourly":
|
||||||
|
filters = [
|
||||||
|
[search_by, "=", name],
|
||||||
|
["modified", ">", frappe.utils.add_to_date(frappe.utils.now_datetime(), 0, 0, 0, -1)],
|
||||||
|
["modified", "<", frappe.utils.now_datetime()]
|
||||||
|
]
|
||||||
|
|
||||||
|
return filters
|
||||||
|
|
@ -9,6 +9,7 @@ import frappe.defaults
|
||||||
import frappe.desk.form.meta
|
import frappe.desk.form.meta
|
||||||
from frappe.model.utils.user_settings import get_user_settings
|
from frappe.model.utils.user_settings import get_user_settings
|
||||||
from frappe.permissions import get_doc_permissions
|
from frappe.permissions import get_doc_permissions
|
||||||
|
from frappe.desk.form.document_follow import is_document_followed
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|
@ -90,7 +91,6 @@ def get_docinfo(doc=None, doctype=None, name=None):
|
||||||
doc = frappe.get_doc(doctype, name)
|
doc = frappe.get_doc(doctype, name)
|
||||||
if not doc.has_permission("read"):
|
if not doc.has_permission("read"):
|
||||||
raise frappe.PermissionError
|
raise frappe.PermissionError
|
||||||
|
|
||||||
frappe.response["docinfo"] = {
|
frappe.response["docinfo"] = {
|
||||||
"attachments": get_attachments(doc.doctype, doc.name),
|
"attachments": get_attachments(doc.doctype, doc.name),
|
||||||
"communications": _get_communications(doc.doctype, doc.name),
|
"communications": _get_communications(doc.doctype, doc.name),
|
||||||
|
|
@ -101,7 +101,9 @@ def get_docinfo(doc=None, doctype=None, name=None):
|
||||||
"permissions": get_doc_permissions(doc),
|
"permissions": get_doc_permissions(doc),
|
||||||
"shared": frappe.share.get_users(doc.doctype, doc.name),
|
"shared": frappe.share.get_users(doc.doctype, doc.name),
|
||||||
"rating": get_feedback_rating(doc.doctype, doc.name),
|
"rating": get_feedback_rating(doc.doctype, doc.name),
|
||||||
"views": get_view_logs(doc.doctype, doc.name)
|
"views": get_view_logs(doc.doctype, doc.name),
|
||||||
|
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
|
||||||
|
"document_follow_enabled": frappe.db.get_value("User", frappe.session.user, "document_follow_notify")
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_attachments(dt, dn):
|
def get_attachments(dt, dn):
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import frappe, json
|
||||||
import frappe.desk.form.meta
|
import frappe.desk.form.meta
|
||||||
import frappe.desk.form.load
|
import frappe.desk.form.load
|
||||||
from frappe.utils.html_utils import clean_email_html
|
from frappe.utils.html_utils import clean_email_html
|
||||||
|
from frappe.desk.form.document_follow import follow_document
|
||||||
|
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from six import string_types
|
from six import string_types
|
||||||
|
|
@ -68,6 +69,7 @@ def add_comment(reference_doctype, reference_name, content, comment_email):
|
||||||
comment_type = 'Comment'
|
comment_type = 'Comment'
|
||||||
)).insert(ignore_permissions = True)
|
)).insert(ignore_permissions = True)
|
||||||
|
|
||||||
|
follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user)
|
||||||
return doc.as_dict()
|
return doc.as_dict()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from __future__ import unicode_literals
|
||||||
import frappe, json
|
import frappe, json
|
||||||
from frappe.database.schema import add_column
|
from frappe.database.schema import add_column
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.desk.form.document_follow import follow_document
|
||||||
from frappe.utils import get_link_to_form
|
from frappe.utils import get_link_to_form
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|
@ -46,7 +47,7 @@ def _toggle_like(doctype, name, add, user=None):
|
||||||
if user not in liked_by:
|
if user not in liked_by:
|
||||||
liked_by.append(user)
|
liked_by.append(user)
|
||||||
add_comment(doctype, name)
|
add_comment(doctype, name)
|
||||||
|
follow_document(doctype, name, user)
|
||||||
else:
|
else:
|
||||||
if user in liked_by:
|
if user in liked_by:
|
||||||
liked_by.remove(user)
|
liked_by.remove(user)
|
||||||
|
|
|
||||||
26
frappe/desk/listview.py
Normal file
26
frappe/desk/listview.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# MIT License. See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_list_settings(doctype):
|
||||||
|
try:
|
||||||
|
return frappe.get_cached_doc("List View Setting", doctype)
|
||||||
|
except frappe.DoesNotExistError:
|
||||||
|
frappe.clear_messages()
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def set_list_settings(doctype, values):
|
||||||
|
try:
|
||||||
|
doc = frappe.get_doc("List View Setting", doctype)
|
||||||
|
except frappe.DoesNotExistError:
|
||||||
|
doc = frappe.new_doc("List View Setting")
|
||||||
|
doc.name = doctype
|
||||||
|
frappe.clear_messages()
|
||||||
|
doc.update(json.loads(values))
|
||||||
|
doc.save()
|
||||||
0
frappe/email/doctype/document_follow/__init__.py
Normal file
0
frappe/email/doctype/document_follow/__init__.py
Normal file
6
frappe/email/doctype/document_follow/document_follow.js
Normal file
6
frappe/email/doctype/document_follow/document_follow.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Document Follow', {
|
||||||
|
|
||||||
|
});
|
||||||
181
frappe/email/doctype/document_follow/document_follow.json
Normal file
181
frappe/email/doctype/document_follow/document_follow.json
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
{
|
||||||
|
"allow_copy": 0,
|
||||||
|
"allow_events_in_timeline": 0,
|
||||||
|
"allow_guest_to_view": 0,
|
||||||
|
"allow_import": 0,
|
||||||
|
"allow_rename": 0,
|
||||||
|
"beta": 0,
|
||||||
|
"creation": "2019-01-09 16:39:23.746535",
|
||||||
|
"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": "ref_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": "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": 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": "ref_docname",
|
||||||
|
"fieldtype": "Dynamic 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 Name",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"options": "ref_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,
|
||||||
|
"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": 1,
|
||||||
|
"is_submittable": 0,
|
||||||
|
"issingle": 0,
|
||||||
|
"istable": 0,
|
||||||
|
"max_attachments": 0,
|
||||||
|
"modified": "2019-02-26 15:43:44.330348",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Email",
|
||||||
|
"name": "Document Follow",
|
||||||
|
"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": "All",
|
||||||
|
"set_user_permissions": 0,
|
||||||
|
"share": 1,
|
||||||
|
"submit": 0,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quick_entry": 0,
|
||||||
|
"read_only": 1,
|
||||||
|
"read_only_onload": 0,
|
||||||
|
"show_name_in_global_search": 0,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 0,
|
||||||
|
"track_seen": 0,
|
||||||
|
"track_views": 0
|
||||||
|
}
|
||||||
10
frappe/email/doctype/document_follow/document_follow.py
Normal file
10
frappe/email/doctype/document_follow/document_follow.py
Normal 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
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class DocumentFollow(Document):
|
||||||
|
pass
|
||||||
|
|
||||||
23
frappe/email/doctype/document_follow/test_document_follow.js
Normal file
23
frappe/email/doctype/document_follow/test_document_follow.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
// rename this file from _test_[name] to test_[name] to activate
|
||||||
|
// and remove above this line
|
||||||
|
|
||||||
|
QUnit.test("test: Document Follow", function (assert) {
|
||||||
|
let done = assert.async();
|
||||||
|
|
||||||
|
// number of asserts
|
||||||
|
assert.expect(1);
|
||||||
|
|
||||||
|
frappe.run_serially([
|
||||||
|
// insert a new Document Follow
|
||||||
|
() => frappe.tests.make('Document Follow', [
|
||||||
|
// values to be set
|
||||||
|
{key: 'value'}
|
||||||
|
]),
|
||||||
|
() => {
|
||||||
|
assert.equal(cur_frm.doc.key, 'value');
|
||||||
|
},
|
||||||
|
() => done()
|
||||||
|
]);
|
||||||
|
|
||||||
|
});
|
||||||
56
frappe/email/doctype/document_follow/test_document_follow.py
Normal file
56
frappe/email/doctype/document_follow/test_document_follow.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import unittest
|
||||||
|
import frappe.desk.form.document_follow as document_follow
|
||||||
|
|
||||||
|
class TestDocumentFollow(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_add_subscription_and_send_mail(self):
|
||||||
|
user = get_user()
|
||||||
|
event_doc = get_event()
|
||||||
|
|
||||||
|
event_doc.description = "This is a test description for sending mail"
|
||||||
|
event_doc.save()
|
||||||
|
|
||||||
|
doc = document_follow.follow_document("Event", event_doc.name , user.name, force=True)
|
||||||
|
self.assertEquals(doc.user, user.name)
|
||||||
|
|
||||||
|
document_follow.send_hourly_updates()
|
||||||
|
|
||||||
|
email_queue_entry_name = frappe.get_all("Email Queue", limit=1)[0].name
|
||||||
|
email_queue_entry_doc = frappe.get_doc("Email Queue", email_queue_entry_name)
|
||||||
|
|
||||||
|
self.assertEquals((email_queue_entry_doc.recipients[0].recipient), user.name)
|
||||||
|
|
||||||
|
self.assertIn(event_doc.doctype, email_queue_entry_doc.message)
|
||||||
|
self.assertIn(event_doc.name, email_queue_entry_doc.message)
|
||||||
|
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def get_event():
|
||||||
|
doc = frappe.get_doc({
|
||||||
|
'doctype': 'Event',
|
||||||
|
'subject': "_Test_Doc_Follow",
|
||||||
|
'doc.starts_on': frappe.utils.now(),
|
||||||
|
'doc.ends_on': frappe.utils.add_days(frappe.utils.now(),5),
|
||||||
|
'doc.description': "Hello"
|
||||||
|
})
|
||||||
|
doc.insert()
|
||||||
|
return doc
|
||||||
|
|
||||||
|
def get_user():
|
||||||
|
doc = frappe.new_doc("User")
|
||||||
|
doc.email = "test@docsub.com"
|
||||||
|
doc.first_name = "Test"
|
||||||
|
doc.last_name = "User"
|
||||||
|
doc.send_welcome_email = 0
|
||||||
|
doc.document_follow_notify = 1
|
||||||
|
doc.document_follow_frequency = "Hourly"
|
||||||
|
doc.insert()
|
||||||
|
return doc
|
||||||
|
|
@ -108,6 +108,7 @@ class TestNotification(unittest.TestCase):
|
||||||
{ "email_by_document_field": "owner" }
|
{ "email_by_document_field": "owner" }
|
||||||
]
|
]
|
||||||
}).insert()
|
}).insert()
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
event = frappe.new_doc("Event")
|
event = frappe.new_doc("Event")
|
||||||
event.subject = "test-2",
|
event.subject = "test-2",
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,8 @@ scheduler_events = {
|
||||||
"frappe.desk.page.backups.backups.delete_downloadable_backups",
|
"frappe.desk.page.backups.backups.delete_downloadable_backups",
|
||||||
"frappe.limits.update_space_usage",
|
"frappe.limits.update_space_usage",
|
||||||
"frappe.desk.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
|
"frappe.desk.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
|
||||||
"frappe.deferred_insert.save_to_db"
|
"frappe.deferred_insert.save_to_db",
|
||||||
|
"frappe.desk.form.document_follow.send_hourly_updates"
|
||||||
],
|
],
|
||||||
"daily": [
|
"daily": [
|
||||||
"frappe.email.queue.clear_outbox",
|
"frappe.email.queue.clear_outbox",
|
||||||
|
|
@ -172,6 +173,7 @@ scheduler_events = {
|
||||||
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
|
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
|
||||||
"frappe.core.doctype.feedback_request.feedback_request.delete_feedback_request",
|
"frappe.core.doctype.feedback_request.feedback_request.delete_feedback_request",
|
||||||
"frappe.core.doctype.activity_log.activity_log.clear_authentication_logs",
|
"frappe.core.doctype.activity_log.activity_log.clear_authentication_logs",
|
||||||
|
"frappe.desk.form.document_follow.send_daily_updates"
|
||||||
],
|
],
|
||||||
"daily_long": [
|
"daily_long": [
|
||||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
|
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
|
||||||
|
|
@ -181,7 +183,8 @@ scheduler_events = {
|
||||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_weekly",
|
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_weekly",
|
||||||
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_weekly",
|
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_weekly",
|
||||||
"frappe.utils.change_log.check_for_update",
|
"frappe.utils.change_log.check_for_update",
|
||||||
"frappe.desk.doctype.route_history.route_history.flush_old_route_records"
|
"frappe.desk.doctype.route_history.route_history.flush_old_route_records",
|
||||||
|
"frappe.desk.form.document_follow.send_weekly_updates"
|
||||||
],
|
],
|
||||||
"monthly": [
|
"monthly": [
|
||||||
"frappe.email.doctype.auto_email_report.auto_email_report.send_monthly"
|
"frappe.email.doctype.auto_email_report.auto_email_report.send_monthly"
|
||||||
|
|
|
||||||
|
|
@ -459,9 +459,12 @@ class BaseDocument(object):
|
||||||
# that are mapped as link_fieldname.source_fieldname in Options of
|
# that are mapped as link_fieldname.source_fieldname in Options of
|
||||||
# Readonly or Data or Text type fields
|
# Readonly or Data or Text type fields
|
||||||
|
|
||||||
# NOTE: All fields will be replaced, if you want manual changes to stay
|
fields_to_fetch = [
|
||||||
# use `frm.add_fetch`
|
_df for _df in self.meta.get_fields_to_fetch(df.fieldname)
|
||||||
fields_to_fetch = self.meta.get_fields_to_fetch(df.fieldname)
|
if
|
||||||
|
not _df.get('fetch_if_empty')
|
||||||
|
or (_df.get('fetch_if_empty') and not self.get(_df.fieldname))
|
||||||
|
]
|
||||||
|
|
||||||
if not fields_to_fetch:
|
if not fields_to_fetch:
|
||||||
# cache a single value type
|
# cache a single value type
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,9 @@ class DatabaseQuery(object):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.fields = [f.strip() for f in self.fields.split(",")]
|
self.fields = [f.strip() for f in self.fields.split(",")]
|
||||||
|
|
||||||
|
# remove empty strings / nulls in fields
|
||||||
|
self.fields = [f for f in self.fields if f]
|
||||||
|
|
||||||
for filter_name in ["filters", "or_filters"]:
|
for filter_name in ["filters", "or_filters"]:
|
||||||
filters = getattr(self, filter_name)
|
filters = getattr(self, filter_name)
|
||||||
if isinstance(filters, string_types):
|
if isinstance(filters, string_types):
|
||||||
|
|
@ -192,7 +195,6 @@ class DatabaseQuery(object):
|
||||||
field which may leads to sql injection.
|
field which may leads to sql injection.
|
||||||
example :
|
example :
|
||||||
field = "`DocType`.`issingle`, version()"
|
field = "`DocType`.`issingle`, version()"
|
||||||
|
|
||||||
As field contains `,` and mysql function `version()`, with the help of regex
|
As field contains `,` and mysql function `version()`, with the help of regex
|
||||||
the system will filter out this field.
|
the system will filter out this field.
|
||||||
'''
|
'''
|
||||||
|
|
@ -326,7 +328,6 @@ class DatabaseQuery(object):
|
||||||
|
|
||||||
def prepare_filter_condition(self, f):
|
def prepare_filter_condition(self, f):
|
||||||
"""Returns a filter condition in the format:
|
"""Returns a filter condition in the format:
|
||||||
|
|
||||||
ifnull(`tabDocType`.`fieldname`, fallback) operator "value"
|
ifnull(`tabDocType`.`fieldname`, fallback) operator "value"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ def check_if_doc_is_linked(doc, method="Delete"):
|
||||||
for item in frappe.db.get_values(link_dt, {link_field:doc.name},
|
for item in frappe.db.get_values(link_dt, {link_field:doc.name},
|
||||||
["name", "parent", "parenttype", "docstatus"], as_dict=True):
|
["name", "parent", "parenttype", "docstatus"], as_dict=True):
|
||||||
linked_doctype = item.parenttype if item.parent else link_dt
|
linked_doctype = item.parenttype if item.parent else link_dt
|
||||||
if linked_doctype in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", 'File', 'Version', "Activity Log", 'Comment'):
|
if linked_doctype in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", 'File', 'Version', "Activity Log", "Document Follow"):
|
||||||
# don't check for communication and todo!
|
# don't check for communication and todo!
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -220,7 +220,7 @@ def check_if_doc_is_linked(doc, method="Delete"):
|
||||||
def check_if_doc_is_dynamically_linked(doc, method="Delete"):
|
def check_if_doc_is_dynamically_linked(doc, method="Delete"):
|
||||||
'''Raise `frappe.LinkExistsError` if the document is dynamically linked'''
|
'''Raise `frappe.LinkExistsError` if the document is dynamically linked'''
|
||||||
for df in get_dynamic_link_map().get(doc.doctype, []):
|
for df in get_dynamic_link_map().get(doc.doctype, []):
|
||||||
if df.parent in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", 'File', 'Version', 'View Log', 'Comment'):
|
if df.parent in ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", 'File', 'Version', 'View Log', "Document Follow"):
|
||||||
# don't check for communication and todo!
|
# don't check for communication and todo!
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -272,6 +272,7 @@ def delete_dynamic_links(doctype, name):
|
||||||
delete_references('Version', doctype, name, 'ref_doctype', 'docname')
|
delete_references('Version', doctype, name, 'ref_doctype', 'docname')
|
||||||
delete_references('Comment', doctype, name)
|
delete_references('Comment', doctype, name)
|
||||||
delete_references('View Log', doctype, name)
|
delete_references('View Log', doctype, name)
|
||||||
|
delete_references('Document Follow', doctype, name, 'ref_doctype', 'ref_docname')
|
||||||
|
|
||||||
# unlink communications
|
# unlink communications
|
||||||
clear_references('Communication', doctype, name)
|
clear_references('Communication', doctype, name)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from frappe.model import optional_fields, table_fields
|
||||||
from frappe.model.workflow import validate_workflow
|
from frappe.model.workflow import validate_workflow
|
||||||
from frappe.utils.global_search import update_global_search
|
from frappe.utils.global_search import update_global_search
|
||||||
from frappe.integrations.doctype.webhook import run_webhooks
|
from frappe.integrations.doctype.webhook import run_webhooks
|
||||||
|
from frappe.desk.form.document_follow import follow_document
|
||||||
|
|
||||||
# once_only validation
|
# once_only validation
|
||||||
# methods
|
# methods
|
||||||
|
|
@ -1014,6 +1015,7 @@ class Document(BaseDocument):
|
||||||
version = frappe.new_doc('Version')
|
version = frappe.new_doc('Version')
|
||||||
if version.set_diff(self._doc_before_save, self):
|
if version.set_diff(self._doc_before_save, self):
|
||||||
version.insert(ignore_permissions=True)
|
version.insert(ignore_permissions=True)
|
||||||
|
follow_document(self.doctype, self.name, frappe.session.user)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def whitelist(f):
|
def whitelist(f):
|
||||||
|
|
@ -1215,7 +1217,7 @@ class Document(BaseDocument):
|
||||||
|
|
||||||
if file_lock.lock_exists(self.get_signature()):
|
if file_lock.lock_exists(self.get_signature()):
|
||||||
frappe.throw(_('This document is currently queued for execution. Please try again'),
|
frappe.throw(_('This document is currently queued for execution. Please try again'),
|
||||||
title=_('Document Queued'), indicator='red')
|
title=_('Document Queued'))
|
||||||
|
|
||||||
self.lock()
|
self.lock()
|
||||||
enqueue('frappe.model.document.execute_action', doctype=self.doctype, name=self.name,
|
enqueue('frappe.model.document.execute_action', doctype=self.doctype, name=self.name,
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,7 @@ class Meta(Document):
|
||||||
are to be fetched and updated for a particular link field
|
are to be fetched and updated for a particular link field
|
||||||
|
|
||||||
These fields are of type Data, Link, Text, Readonly and their
|
These fields are of type Data, Link, Text, Readonly and their
|
||||||
options property is set as `link_fieldname`.`source_fieldname`'''
|
fetch_from property is set as `link_fieldname`.`source_fieldname`'''
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,7 @@ frappe.patches.v11_0.set_missing_creation_and_modified_value_for_user_permission
|
||||||
frappe.patches.v11_0.remove_doctype_user_permissions_for_page_and_report
|
frappe.patches.v11_0.remove_doctype_user_permissions_for_page_and_report
|
||||||
frappe.patches.v11_0.set_default_letter_head_source
|
frappe.patches.v11_0.set_default_letter_head_source
|
||||||
frappe.patches.v12_0.set_primary_key_in_series
|
frappe.patches.v12_0.set_primary_key_in_series
|
||||||
|
execute:frappe.reload_doc('email', 'doctype', 'document_follow')
|
||||||
execute:frappe.delete_doc("Page", "modules", ignore_missing=True)
|
execute:frappe.delete_doc("Page", "modules", ignore_missing=True)
|
||||||
frappe.patches.v11_0.set_default_letter_head_source
|
frappe.patches.v11_0.set_default_letter_head_source
|
||||||
frappe.patches.v12_0.setup_comments_from_communications
|
frappe.patches.v12_0.setup_comments_from_communications
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,4 @@ def execute():
|
||||||
user.first_name = re.sub("[<>]+", '', frappe.safe_decode(user.first_name))
|
user.first_name = re.sub("[<>]+", '', frappe.safe_decode(user.first_name))
|
||||||
if user.last_name:
|
if user.last_name:
|
||||||
user.last_name = re.sub("[<>]+", '', frappe.safe_decode(user.last_name))
|
user.last_name = re.sub("[<>]+", '', frappe.safe_decode(user.last_name))
|
||||||
create_contact(user, ignore_links=True)
|
create_contact(user, ignore_links=True, ignore_mandatory=True)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
def execute():
|
def execute():
|
||||||
|
frappe.reload_doctype("Comment")
|
||||||
|
|
||||||
for comment in frappe.get_all('Communication', fields = ['*'],
|
for comment in frappe.get_all('Communication', fields = ['*'],
|
||||||
filters = dict(communication_type = 'Comment')):
|
filters = dict(communication_type = 'Comment')):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,7 @@
|
||||||
],
|
],
|
||||||
"js/form.min.js": [
|
"js/form.min.js": [
|
||||||
"public/js/frappe/form/templates/print_layout.html",
|
"public/js/frappe/form/templates/print_layout.html",
|
||||||
|
"public/js/frappe/form/document_follow.js",
|
||||||
"public/js/frappe/form/templates/users_in_sidebar.html",
|
"public/js/frappe/form/templates/users_in_sidebar.html",
|
||||||
"public/js/frappe/form/templates/set_sharing.html",
|
"public/js/frappe/form/templates/set_sharing.html",
|
||||||
"public/js/frappe/form/templates/form_sidebar.html",
|
"public/js/frappe/form/templates/form_sidebar.html",
|
||||||
|
|
@ -326,11 +327,6 @@
|
||||||
"public/js/frappe/list/list_sidebar.js",
|
"public/js/frappe/list/list_sidebar.js",
|
||||||
"public/js/frappe/list/list_sidebar.html",
|
"public/js/frappe/list/list_sidebar.html",
|
||||||
"public/js/frappe/list/list_sidebar_stat.html",
|
"public/js/frappe/list/list_sidebar_stat.html",
|
||||||
"public/js/frappe/list/list_item_main.html",
|
|
||||||
"public/js/frappe/list/list_item_row.html",
|
|
||||||
"public/js/frappe/list/list_item_main_head.html",
|
|
||||||
"public/js/frappe/list/list_item_row_head.html",
|
|
||||||
"public/js/frappe/list/list_item_subject.html",
|
|
||||||
"public/js/frappe/list/list_view_permission_restrictions.html",
|
"public/js/frappe/list/list_view_permission_restrictions.html",
|
||||||
|
|
||||||
"public/js/frappe/views/gantt/gantt_view.js",
|
"public/js/frappe/views/gantt/gantt_view.js",
|
||||||
|
|
@ -340,8 +336,6 @@
|
||||||
"public/js/frappe/views/inbox/inbox_view.js",
|
"public/js/frappe/views/inbox/inbox_view.js",
|
||||||
"public/js/frappe/views/file/file_view.js",
|
"public/js/frappe/views/file/file_view.js",
|
||||||
|
|
||||||
"public/js/frappe/list/header_select_all_like_filter.html",
|
|
||||||
"public/js/frappe/list/item_assigned_to_comment_count.html",
|
|
||||||
"public/js/frappe/views/treeview.js",
|
"public/js/frappe/views/treeview.js",
|
||||||
"public/js/frappe/views/interaction.js",
|
"public/js/frappe/views/interaction.js",
|
||||||
|
|
||||||
|
|
@ -378,7 +372,9 @@
|
||||||
"public/less/list.less",
|
"public/less/list.less",
|
||||||
"website/css/web_form.css",
|
"website/css/web_form.css",
|
||||||
"public/less/quill.less",
|
"public/less/quill.less",
|
||||||
"public/less/datepicker.less"
|
"public/less/datepicker.less",
|
||||||
|
"public/less/awesomplete.less",
|
||||||
|
"public/less/form_grid.less"
|
||||||
],
|
],
|
||||||
"js/print_format_v3.min.js": [
|
"js/print_format_v3.min.js": [
|
||||||
"public/js/legacy/layout.js",
|
"public/js/legacy/layout.js",
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,10 @@ hr {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
.list-unstyled {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
/* auto email report */
|
/* auto email report */
|
||||||
.report-title {
|
.report-title {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,25 @@ Table.create = (value) => {
|
||||||
}
|
}
|
||||||
Quill.register(Table, true);
|
Quill.register(Table, true);
|
||||||
|
|
||||||
|
// link without href
|
||||||
|
var Link = Quill.import('formats/link');
|
||||||
|
|
||||||
|
class MyLink extends Link {
|
||||||
|
static create(value) {
|
||||||
|
let node = super.create(value);
|
||||||
|
value = this.sanitize(value);
|
||||||
|
node.setAttribute('href', value);
|
||||||
|
if(value.startsWith('/') || value.indexOf(window.location.host)) {
|
||||||
|
// no href if internal link
|
||||||
|
node.removeAttribute('target');
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Quill.register(MyLink);
|
||||||
|
|
||||||
|
|
||||||
// hidden blot
|
// hidden blot
|
||||||
class HiddenBlock extends Block {
|
class HiddenBlock extends Block {
|
||||||
static create(value) {
|
static create(value) {
|
||||||
|
|
@ -44,13 +63,11 @@ Uploader.DEFAULTS.mimetypes.push('image/gif');
|
||||||
// inline style
|
// inline style
|
||||||
const BackgroundStyle = Quill.import('attributors/style/background');
|
const BackgroundStyle = Quill.import('attributors/style/background');
|
||||||
const ColorStyle = Quill.import('attributors/style/color');
|
const ColorStyle = Quill.import('attributors/style/color');
|
||||||
const SizeStyle = Quill.import('attributors/style/size');
|
|
||||||
const FontStyle = Quill.import('attributors/style/font');
|
const FontStyle = Quill.import('attributors/style/font');
|
||||||
const AlignStyle = Quill.import('attributors/style/align');
|
const AlignStyle = Quill.import('attributors/style/align');
|
||||||
const DirectionStyle = Quill.import('attributors/style/direction');
|
const DirectionStyle = Quill.import('attributors/style/direction');
|
||||||
Quill.register(BackgroundStyle, true);
|
Quill.register(BackgroundStyle, true);
|
||||||
Quill.register(ColorStyle, true);
|
Quill.register(ColorStyle, true);
|
||||||
Quill.register(SizeStyle, true);
|
|
||||||
Quill.register(FontStyle, true);
|
Quill.register(FontStyle, true);
|
||||||
Quill.register(AlignStyle, true);
|
Quill.register(AlignStyle, true);
|
||||||
Quill.register(DirectionStyle, true);
|
Quill.register(DirectionStyle, true);
|
||||||
|
|
@ -140,6 +157,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
|
||||||
return [
|
return [
|
||||||
[{ 'header': [1, 2, 3, false] }],
|
[{ 'header': [1, 2, 3, false] }],
|
||||||
['bold', 'italic', 'underline'],
|
['bold', 'italic', 'underline'],
|
||||||
|
[{ 'color': [] }, { 'background': [] }],
|
||||||
['blockquote', 'code-block'],
|
['blockquote', 'code-block'],
|
||||||
['link', 'image'],
|
['link', 'image'],
|
||||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||||
|
|
|
||||||
144
frappe/public/js/frappe/form/document_follow.js
Normal file
144
frappe/public/js/frappe/form/document_follow.js
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
// MIT License. See license.txt
|
||||||
|
|
||||||
|
frappe.provide('frappe.ui.form');
|
||||||
|
|
||||||
|
frappe.ui.form.DocumentFollow = class DocumentFollow {
|
||||||
|
constructor(opts) {
|
||||||
|
$.extend(this, opts);
|
||||||
|
this.follow_document_link = this.parent.find('.follow-document-link');
|
||||||
|
this.unfollow_document_link = this.parent.find('.unfollow-document-link');
|
||||||
|
this.follow_span = this.parent.find('.anchor-document-follow > span');
|
||||||
|
this.followed_by = this.parent.find('.followed-by');
|
||||||
|
this.followed_by_label = this.parent.find('.followed-by-label');
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.set_followers();
|
||||||
|
this.render_sidebar();
|
||||||
|
}
|
||||||
|
|
||||||
|
render_sidebar() {
|
||||||
|
const docinfo = this.frm.get_docinfo();
|
||||||
|
const document_follow_enabled = docinfo && docinfo.document_follow_enabled;
|
||||||
|
const document_can_be_followed = frappe.get_meta(this.frm.doctype).track_changes;
|
||||||
|
if (frappe.session.user === 'Administrator'
|
||||||
|
|| !document_follow_enabled
|
||||||
|
|| !document_can_be_followed
|
||||||
|
) {
|
||||||
|
this.hide_follow_section();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.bind_events();
|
||||||
|
|
||||||
|
const is_followed = docinfo && docinfo.is_document_followed;
|
||||||
|
|
||||||
|
if(is_followed > 0) {
|
||||||
|
this.unfollow_document_link.removeClass('hidden');
|
||||||
|
this.follow_document_link.addClass('hidden');
|
||||||
|
} else {
|
||||||
|
this.followed_by_label.addClass('hidden');
|
||||||
|
this.followed_by.addClass('hidden');
|
||||||
|
this.unfollow_document_link.addClass('hidden');
|
||||||
|
this.follow_document_link.removeClass('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bind_events() {
|
||||||
|
this.follow_document_link.on('click', () => {
|
||||||
|
this.follow_document_link.addClass('text-muted disable-click');
|
||||||
|
frappe.call({
|
||||||
|
method: 'frappe.desk.form.document_follow.follow_document',
|
||||||
|
args: {
|
||||||
|
'doctype': this.frm.doctype,
|
||||||
|
'doc_name': this.frm.doc.name,
|
||||||
|
'user': frappe.session.user,
|
||||||
|
'force': true
|
||||||
|
},
|
||||||
|
callback: (r) => {
|
||||||
|
if (r.message) {
|
||||||
|
this.follow_action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.unfollow_document_link.on('click', () => {
|
||||||
|
this.unfollow_document_link.addClass('text-muted disable-click');
|
||||||
|
frappe.call({
|
||||||
|
method: 'frappe.desk.form.document_follow.unfollow_document',
|
||||||
|
args: {
|
||||||
|
'doctype': this.frm.doctype,
|
||||||
|
'doc_name': this.frm.doc.name,
|
||||||
|
'user': frappe.session.user
|
||||||
|
},
|
||||||
|
callback: (r) => {
|
||||||
|
if(r.message) {
|
||||||
|
this.unfollow_action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hide_follow_section() {
|
||||||
|
this.parent.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
set_followers() {
|
||||||
|
this.followed_by.removeClass('hidden');
|
||||||
|
this.followed_by_label.removeClass('hidden');
|
||||||
|
this.followed_by.empty();
|
||||||
|
this.get_followed_user().then(user => {
|
||||||
|
$(user).appendTo(this.followed_by);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get_followed_user() {
|
||||||
|
var html = '';
|
||||||
|
return new Promise(resolve => {
|
||||||
|
frappe.call({
|
||||||
|
method: 'frappe.desk.form.document_follow.get_follow_users',
|
||||||
|
args: {
|
||||||
|
'doctype': this.frm.doctype,
|
||||||
|
'doc_name': this.frm.doc.name,
|
||||||
|
},
|
||||||
|
}).then(r => {
|
||||||
|
this.count_others = 0;
|
||||||
|
for (var d in r.message) {
|
||||||
|
this.count_others++;
|
||||||
|
if(this.count_others < 4){
|
||||||
|
html += frappe.avatar(r.message[d].user, 'avatar-small');
|
||||||
|
}
|
||||||
|
if(this.count_others === 0){
|
||||||
|
this.followed_by.addClass('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(html);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
follow_action() {
|
||||||
|
frappe.show_alert({
|
||||||
|
message: __('You are now following this document. You will receive daily updates via email. You can change this in User Settings.'),
|
||||||
|
indicator: 'orange'
|
||||||
|
});
|
||||||
|
this.follow_document_link.removeClass('text-muted disable-click');
|
||||||
|
this.follow_document_link.addClass('hidden');
|
||||||
|
this.unfollow_document_link.removeClass('hidden');
|
||||||
|
this.set_followers();
|
||||||
|
}
|
||||||
|
|
||||||
|
unfollow_action() {
|
||||||
|
frappe.show_alert({
|
||||||
|
message: __('You unfollowed this document'),
|
||||||
|
indicator: 'red'
|
||||||
|
});
|
||||||
|
this.unfollow_document_link.removeClass('text-muted disable-click');
|
||||||
|
this.unfollow_document_link.addClass('hidden');
|
||||||
|
this.follow_document_link.removeClass('hidden');
|
||||||
|
this.followed_by.addClass('hidden');
|
||||||
|
this.followed_by_label.addClass('hidden');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -308,7 +308,7 @@ frappe.ui.form.Timeline = class Timeline {
|
||||||
}
|
}
|
||||||
|
|
||||||
prepare_timeline_item(c) {
|
prepare_timeline_item(c) {
|
||||||
if(!c.sender) c.sender = c.owner;
|
if(!c.sender) c.sender = c.owner || 'Guest';
|
||||||
|
|
||||||
if(c.sender && c.sender.indexOf("<")!==-1) {
|
if(c.sender && c.sender.indexOf("<")!==-1) {
|
||||||
c.sender = c.sender.split("<")[1].split(">")[0];
|
c.sender = c.sender.split("<")[1].split(">")[0];
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,13 @@ frappe.ui.form.Sidebar = Class.extend({
|
||||||
this.user_actions = this.sidebar.find(".user-actions");
|
this.user_actions = this.sidebar.find(".user-actions");
|
||||||
this.image_section = this.sidebar.find(".sidebar-image-section");
|
this.image_section = this.sidebar.find(".sidebar-image-section");
|
||||||
this.image_wrapper = this.image_section.find('.sidebar-image-wrapper');
|
this.image_wrapper = this.image_section.find('.sidebar-image-wrapper');
|
||||||
|
|
||||||
this.make_assignments();
|
this.make_assignments();
|
||||||
this.make_attachments();
|
this.make_attachments();
|
||||||
this.make_shared();
|
this.make_shared();
|
||||||
this.make_viewers();
|
this.make_viewers();
|
||||||
this.make_tags();
|
this.make_tags();
|
||||||
this.make_like();
|
this.make_like();
|
||||||
|
this.make_follow();
|
||||||
|
|
||||||
this.bind_events();
|
this.bind_events();
|
||||||
frappe.ui.form.setup_user_image_event(this.frm);
|
frappe.ui.form.setup_user_image_event(this.frm);
|
||||||
|
|
@ -54,6 +54,7 @@ frappe.ui.form.Sidebar = Class.extend({
|
||||||
this.frm.assign_to.refresh();
|
this.frm.assign_to.refresh();
|
||||||
this.frm.attachments.refresh();
|
this.frm.attachments.refresh();
|
||||||
this.frm.shared.refresh();
|
this.frm.shared.refresh();
|
||||||
|
this.frm.follow.refresh();
|
||||||
this.frm.viewers.refresh();
|
this.frm.viewers.refresh();
|
||||||
this.frm.tags && this.frm.tags.refresh(this.frm.doc._user_tags);
|
this.frm.tags && this.frm.tags.refresh(this.frm.doc._user_tags);
|
||||||
this.sidebar.find(".modified-by").html(__("{0} edited this {1}",
|
this.sidebar.find(".modified-by").html(__("{0} edited this {1}",
|
||||||
|
|
@ -131,7 +132,12 @@ frappe.ui.form.Sidebar = Class.extend({
|
||||||
this.like_count = this.sidebar.find(".liked-by .likes-count");
|
this.like_count = this.sidebar.find(".liked-by .likes-count");
|
||||||
frappe.ui.setup_like_popover(this.sidebar.find(".liked-by-parent"), ".liked-by");
|
frappe.ui.setup_like_popover(this.sidebar.find(".liked-by-parent"), ".liked-by");
|
||||||
},
|
},
|
||||||
|
make_follow: function(){
|
||||||
|
this.frm.follow = new frappe.ui.form.DocumentFollow({
|
||||||
|
frm: this.frm,
|
||||||
|
parent: this.sidebar.find(".followed-by-section")
|
||||||
|
});
|
||||||
|
},
|
||||||
refresh_like: function() {
|
refresh_like: function() {
|
||||||
if (!this.like_icon) {
|
if (!this.like_icon) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -149,7 +155,6 @@ frappe.ui.form.Sidebar = Class.extend({
|
||||||
|
|
||||||
refresh_image: function() {
|
refresh_image: function() {
|
||||||
},
|
},
|
||||||
|
|
||||||
setup_ratings: function() {
|
setup_ratings: function() {
|
||||||
var _ratings = this.frm.get_docinfo().rating || 0;
|
var _ratings = this.frm.get_docinfo().rating || 0;
|
||||||
|
|
||||||
|
|
@ -158,5 +163,5 @@ frappe.ui.form.Sidebar = Class.extend({
|
||||||
var rating_icons = frappe.render_template("rating_icons", {rating: _ratings, show_label: false});
|
var rating_icons = frappe.render_template("rating_icons", {rating: _ratings, show_label: false});
|
||||||
this.ratings.find(".rating-icons").html(rating_icons);
|
this.ratings.find(".rating-icons").html(rating_icons);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -59,30 +59,30 @@ export default class Grid {
|
||||||
<div class="grid-body">
|
<div class="grid-body">
|
||||||
<div class="rows"></div>
|
<div class="rows"></div>
|
||||||
<div class="grid-empty text-center hide">${__("No Data")}</div>
|
<div class="grid-empty text-center hide">${__("No Data")}</div>
|
||||||
<div class="small form-clickable-section grid-footer">
|
</div>
|
||||||
<div class="row">
|
</div>
|
||||||
<div class="col-sm-6 grid-buttons">
|
<div class="mt-2 small form-clickable-section grid-footer">
|
||||||
<button type="reset"
|
<div class="row">
|
||||||
class="btn btn-xs btn-danger grid-remove-rows hide"
|
<div class="col-sm-6 grid-buttons">
|
||||||
style="margin-right: 4px;">
|
<button type="reset"
|
||||||
${__("Delete")}</button>
|
class="btn btn-xs btn-danger grid-remove-rows hide"
|
||||||
<button type="reset"
|
style="margin-right: 4px;">
|
||||||
class="grid-add-multiple-rows btn btn-xs btn-default hide"
|
${__("Delete")}</button>
|
||||||
style="margin-right: 4px;">
|
<button type="reset"
|
||||||
${__("Add Multiple")}</a>
|
class="grid-add-multiple-rows btn btn-xs btn-default hide"
|
||||||
<!-- hack to allow firefox include this in tabs -->
|
style="margin-right: 4px;">
|
||||||
<button type="reset" class="btn btn-xs btn-default grid-add-row">
|
${__("Add Multiple")}</a>
|
||||||
${__("Add Row")}</button>
|
<!-- hack to allow firefox include this in tabs -->
|
||||||
</div>
|
<button type="reset" class="btn btn-xs btn-default grid-add-row">
|
||||||
<div class="col-sm-6 text-right">
|
${__("Add Row")}</button>
|
||||||
<a href="#" class="grid-download btn btn-xs btn-default hide"
|
</div>
|
||||||
style="margin-left: 10px;">
|
<div class="col-sm-6 text-right">
|
||||||
${__("Download")}</a>
|
<a href="#" class="grid-download btn btn-xs btn-default hide"
|
||||||
<a href="#" class="grid-upload btn btn-xs btn-default hide"
|
style="margin-left: 10px;">
|
||||||
style="margin-left: 10px;">
|
${__("Download")}</a>
|
||||||
${__("Upload")}</a>
|
<a href="#" class="grid-upload btn btn-xs btn-default hide"
|
||||||
</div>
|
style="margin-left: 10px;">
|
||||||
</div>
|
${__("Upload")}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -369,7 +369,7 @@ export default class Grid {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
get_modal_data() {
|
get_modal_data() {
|
||||||
return this.df.get_data ? this.df.get_data().filter(data => {
|
return this.df.get_data() ? this.df.get_data().filter(data => {
|
||||||
if (!this.deleted_docs || !in_list(this.deleted_docs, data.name)) {
|
if (!this.deleted_docs || !in_list(this.deleted_docs, data.name)) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,13 +68,27 @@
|
||||||
<li class="h6 viewers-label">{%= __("Currently Viewing") %}</li>
|
<li class="h6 viewers-label">{%= __("Currently Viewing") %}</li>
|
||||||
<li class="form-viewers"></li>
|
<li class="form-viewers"></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="list-unstyled sidebar-menu text-muted">
|
<ul class="list-unstyled sidebar-menu">
|
||||||
<li class="liked-by-parent">
|
<li class="liked-by-parent">
|
||||||
<span class="liked-by">
|
<span class="liked-by">
|
||||||
<i class="octicon octicon-heart like-action text-extra-muted fa-fw"></i>
|
<i class="octicon octicon-heart like-action text-extra-muted fa-fw"></i>
|
||||||
<span class="likes-count"></span>
|
<span class="likes-count"></span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="list-unstyled sidebar-menu followed-by-section">
|
||||||
|
<li class="h6 followed-by-label text-medium hidden">{%= __("Followed by") %}</li>
|
||||||
|
<li class="followed-by"></li>
|
||||||
|
<li class="document-follow">
|
||||||
|
<a class="strong badge-hover follow-document-link hidden">
|
||||||
|
{%= __("Follow") %}
|
||||||
|
</a>
|
||||||
|
<a class="strong badge-hover unfollow-document-link hidden">
|
||||||
|
{%= __("Unfollow") %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="list-unstyled sidebar-menu text-muted">
|
||||||
<li class="modified-by"></li>
|
<li class="modified-by"></li>
|
||||||
<li class="created-by"></li>
|
<li class="created-by"></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{% if (_checkbox) { %}
|
|
||||||
<input class="list-select-all hidden-xs" type="checkbox"
|
|
||||||
title="{%= __("Select All") %}">
|
|
||||||
{% } %}
|
|
||||||
<span class="liked-by-filter-button">
|
|
||||||
<i class="fa-fw octicon octicon-heart text-extra-muted not-liked like-action list-liked-by-me"
|
|
||||||
title="{%= __("Likes") %}"></i>
|
|
||||||
</span>
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<span class="list-row-modified text-muted">
|
|
||||||
{%= comment_when(data.modified, true) %}
|
|
||||||
</span>
|
|
||||||
{% if (data._assign_list.length) { %}
|
|
||||||
<span class="filterable"
|
|
||||||
data-filter="_assign,like,%{%= data._assign_list[data._assign_list.length - 1] %}%">
|
|
||||||
{%= frappe.avatar(data._assign_list[data._assign_list.length - 1]) %}</span>
|
|
||||||
{% } else { %}
|
|
||||||
<span class="avatar avatar-small avatar-empty"></span>
|
|
||||||
{% } %}
|
|
||||||
<span class="list-comment-count small
|
|
||||||
{% if(!data._comment_count) { %} text-extra-muted {% } else { %} text-muted {% } %}">
|
|
||||||
<i class="octicon octicon-comment-discussion"></i>
|
|
||||||
{%= (data._comment_count > 99 ? "99+" : data._comment_count) || 0 %}
|
|
||||||
</span>
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<div class="list-item__content ellipsis
|
|
||||||
{% if(col.type==="Subject") { %}
|
|
||||||
list-item__content--flex-2
|
|
||||||
{% } else { %}
|
|
||||||
hidden-xs
|
|
||||||
{% } %}
|
|
||||||
{% if(col.df && ["Int", "Float", "Currency", "Percent"].indexOf(col.df.fieldtype)!==-1) { %}text-right{% } %}"
|
|
||||||
{% if(col.type!=="Indicator" && col.title) { %}
|
|
||||||
title="{%= col.title + ": " + value %}"
|
|
||||||
{% } %}
|
|
||||||
>
|
|
||||||
{% if (col.type==="Subject") { %}
|
|
||||||
{%= subject %}
|
|
||||||
{% } else if (col.type==="Indicator") { %}
|
|
||||||
{%= indicator %}
|
|
||||||
{% } else if (col.render) { %}
|
|
||||||
{%= col.render(data) %}
|
|
||||||
{% } else if (col.fieldtype==="Image") { %}
|
|
||||||
{% if(data[col.df.options]) { %}
|
|
||||||
<img src="{%= data[col.df.options] %}" style="max-height: 30px; max-width: 100%;">
|
|
||||||
{% } else { %}
|
|
||||||
<div class="missing-image small"><span class="octicon octicon-circle-slash"></span></div>
|
|
||||||
{% } %}
|
|
||||||
{% } else if(col.fieldtype==="Select") { %}
|
|
||||||
<span class="filterable indicator {%= frappe.utils.guess_colour(value) %} ellipsis"
|
|
||||||
data-filter="{%= col.fieldname %},=,{%= value %}">{%= __(value) %}</span>
|
|
||||||
{% } else if(col.fieldtype==="Link") { %}
|
|
||||||
<a class="filterable text-muted grey ellipsis"
|
|
||||||
data-filter="{%= col.fieldname %},=,{%= value %}">{%= value %}</a>
|
|
||||||
{% } else { %}
|
|
||||||
<a class="filterable text-muted ellipsis"
|
|
||||||
data-filter="{%= col.fieldname %},=,{%= value %}">
|
|
||||||
{% if(formatters && formatters[col.fieldname]) { %}
|
|
||||||
{{ formatters[col.fieldname](value, col.df, data) }}
|
|
||||||
{% } else if(col.fieldtype == "Code") { %}
|
|
||||||
{{ value }}
|
|
||||||
{% } else { %}
|
|
||||||
{{ frappe.format(value, col.df, null, data) }}
|
|
||||||
{% } %}
|
|
||||||
</a>
|
|
||||||
{% } %}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
<div class="list-item__content ellipsis text-muted
|
|
||||||
{% if(col.type==="Subject") { %}
|
|
||||||
list-item__content--flex-2
|
|
||||||
{% } else { %}
|
|
||||||
hidden-xs
|
|
||||||
{% } %}
|
|
||||||
{% if(col.df && ["Int", "Float", "Currency", "Percent"].indexOf(col.df.fieldtype)!==-1) { %}text-right{% } %}"
|
|
||||||
>
|
|
||||||
|
|
||||||
{% if (col.type==="Subject") { %}
|
|
||||||
{%= frappe.render_template("header_select_all_like_filter", { _checkbox: _checkbox }) %}
|
|
||||||
{% } %}
|
|
||||||
<span class="list-col-title ellipsis">{{ __(col.title) || __(col.label) || "" }}</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<div class="list-item">
|
|
||||||
{%= main %}
|
|
||||||
|
|
||||||
<!-- id -->
|
|
||||||
{% if (meta.title_field && !settings.hide_name_column) {
|
|
||||||
var is_different = data.name !== data[meta.title_field];
|
|
||||||
%}
|
|
||||||
<div class="list-item__content list-item__content--id hidden-xs hidden-sm ellipsis">
|
|
||||||
{% if (is_different) { %}
|
|
||||||
<a class="text-muted ellipsis" href="#Form/{%= data._doctype_encoded %}/{%= data._name_encoded %}">
|
|
||||||
{%= data.name %}</a>
|
|
||||||
{% } %}
|
|
||||||
</div>
|
|
||||||
{% } %}
|
|
||||||
|
|
||||||
<!-- comment -->
|
|
||||||
{% if (!data._hide_activity) { %}
|
|
||||||
<div class="list-item__content list-item__content--activity hidden-xs">
|
|
||||||
<!-- comments count and assigned to section -->
|
|
||||||
{%= frappe.render_template("item_assigned_to_comment_count", { data: data }) %}
|
|
||||||
</div>
|
|
||||||
<div class="list-item__content list-item__content--indicator visible-xs text-right">
|
|
||||||
{%= indicator_dot %}
|
|
||||||
</div>
|
|
||||||
{% } %}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<div class="list-item list-item--head" data-list-renderer="{{list.name}}">
|
|
||||||
<!-- title + columns -->
|
|
||||||
{%= main %}
|
|
||||||
|
|
||||||
<!-- id -->
|
|
||||||
{% if(list.meta.title_field && !list.settings.hide_name_column) { %}
|
|
||||||
<div class="list-item__content hidden-xs hidden-sm"></div>
|
|
||||||
{% } %}
|
|
||||||
<!-- comment -->
|
|
||||||
<div class="list-item__content list-item__content--activity hidden-xs text-right list-row-right"></div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
{% if (_checkbox) { %}
|
|
||||||
<input class="list-row-checkbox hidden-xs" type="checkbox" data-name="{{name}}">
|
|
||||||
{% } %}
|
|
||||||
{% if (!_hide_activity) { %}
|
|
||||||
<span class="liked-by" data-liked-by=\'{{ JSON.stringify(_liked_by) }}\'>
|
|
||||||
<i class="octicon octicon-heart
|
|
||||||
{% if (_liked_by.indexOf(_user)===-1) { %}
|
|
||||||
text-extra-muted not-liked
|
|
||||||
{% }%}
|
|
||||||
fa-fw like-action"
|
|
||||||
data-name="{{ _name }}" data-doctype="{{ doctype }}">
|
|
||||||
</i>
|
|
||||||
<span class="likes-count">{{ (_liked_by.length > 99 ? "99+" : _liked_by.length) || "" }}</span>
|
|
||||||
</span>
|
|
||||||
{% } %}
|
|
||||||
|
|
||||||
{% var anchor_title = (_full_title).replace(/["]/g, "&\quot;"); %}
|
|
||||||
<a class="grey list-id {{ css_seen }} ellipsis"
|
|
||||||
data-name="{{ _name }}"
|
|
||||||
href="#Form/{{ _doctype_encoded }}/{{ _name_encoded }}"
|
|
||||||
title="{{ anchor_title }}">{{ strip_html(_title) }}</a>
|
|
||||||
{% if (_workflow && !_without_workflow) { %}
|
|
||||||
<span class="label label-{{ _workflow.style }} filterable"
|
|
||||||
data-filter="{{ _workflow.fieldname }},=,{{ _workflow.value }}">
|
|
||||||
{%= _workflow.value %}</span>
|
|
||||||
{% } %}
|
|
||||||
|
|
@ -1,635 +0,0 @@
|
||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
// MIT License. See license.txt
|
|
||||||
|
|
||||||
frappe.provide('frappe.views');
|
|
||||||
|
|
||||||
// Renders customized list
|
|
||||||
// usually based on `in_list_view` property
|
|
||||||
|
|
||||||
frappe.views.ListRenderer = Class.extend({
|
|
||||||
name: 'List',
|
|
||||||
init: function (opts) {
|
|
||||||
$.extend(this, opts);
|
|
||||||
this.meta = frappe.get_meta(this.doctype);
|
|
||||||
|
|
||||||
this.init_settings();
|
|
||||||
this.set_defaults();
|
|
||||||
this.set_fields();
|
|
||||||
this.set_columns();
|
|
||||||
this.setup_cache();
|
|
||||||
},
|
|
||||||
set_defaults: function () {
|
|
||||||
var me = this;
|
|
||||||
this.page_title = __(this.doctype);
|
|
||||||
|
|
||||||
this.set_wrapper();
|
|
||||||
this.setup_filterable();
|
|
||||||
this.prepare_render_view();
|
|
||||||
|
|
||||||
// flag to enable/disable realtime updates in list_view
|
|
||||||
this.no_realtime = false;
|
|
||||||
|
|
||||||
// set false to render view even if no results
|
|
||||||
// e.g Calendar
|
|
||||||
this.show_no_result = true;
|
|
||||||
|
|
||||||
// hide sort selector
|
|
||||||
this.hide_sort_selector = false;
|
|
||||||
|
|
||||||
// default settings
|
|
||||||
this.order_by = this.order_by || 'modified desc';
|
|
||||||
this.filters = this.filters || [];
|
|
||||||
this.or_filters = this.or_filters || [];
|
|
||||||
this.page_length = this.page_length || 20;
|
|
||||||
},
|
|
||||||
setup_cache: function () {
|
|
||||||
frappe.provide('frappe.views.list_renderers.' + this.doctype);
|
|
||||||
frappe.views.list_renderers[this.doctype][this.list_view.current_view] = this;
|
|
||||||
},
|
|
||||||
init_settings: function () {
|
|
||||||
this.settings = frappe.listview_settings[this.doctype] || {};
|
|
||||||
if(!("selectable" in this.settings)) {
|
|
||||||
this.settings.selectable = true;
|
|
||||||
}
|
|
||||||
this.init_user_settings();
|
|
||||||
|
|
||||||
this.order_by = this.user_settings.order_by || this.settings.order_by;
|
|
||||||
this.filters = this.user_settings.filters || this.settings.filters;
|
|
||||||
this.page_length = this.settings.page_length;
|
|
||||||
|
|
||||||
// default filter for submittable doctype
|
|
||||||
if(frappe.model.is_submittable(this.doctype) && (!this.filters || !this.filters.length)) {
|
|
||||||
this.filters = [[this.doctype, "docstatus", "!=", 2]];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
init_user_settings: function () {
|
|
||||||
frappe.provide('frappe.model.user_settings.' + this.doctype + '.' + this.name);
|
|
||||||
this.user_settings = frappe.get_user_settings(this.doctype)[this.name];
|
|
||||||
},
|
|
||||||
after_refresh: function() {
|
|
||||||
// called after refresh in list_view
|
|
||||||
},
|
|
||||||
before_refresh: function() {
|
|
||||||
// called before refresh in list_view
|
|
||||||
},
|
|
||||||
should_refresh: function() {
|
|
||||||
return this.list_view.current_view !== this.list_view.last_view;
|
|
||||||
},
|
|
||||||
load_last_view: function() {
|
|
||||||
// this function should handle loading the last view of your list_renderer,
|
|
||||||
// If you have a last view (for e.g last kanban board in Kanban View),
|
|
||||||
// load it using frappe.set_route and return true
|
|
||||||
// else return false
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
set_wrapper: function () {
|
|
||||||
this.wrapper = this.list_view.wrapper && this.list_view.wrapper.find('.result-list');
|
|
||||||
},
|
|
||||||
set_fields: function () {
|
|
||||||
var me = this;
|
|
||||||
var tabDoctype = '`tab' + this.doctype + '`.';
|
|
||||||
this.fields = [];
|
|
||||||
this.stats = ['_user_tags'];
|
|
||||||
|
|
||||||
var add_field = function (fieldname) {
|
|
||||||
if (!fieldname.includes('`tab')) {
|
|
||||||
fieldname = tabDoctype + '`' + fieldname + '`';
|
|
||||||
}
|
|
||||||
if (!me.fields.includes(fieldname))
|
|
||||||
me.fields.push(fieldname);
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaults = [
|
|
||||||
'name',
|
|
||||||
'owner',
|
|
||||||
'docstatus',
|
|
||||||
'_user_tags',
|
|
||||||
'_comments',
|
|
||||||
'modified',
|
|
||||||
'modified_by',
|
|
||||||
'_assign',
|
|
||||||
'_liked_by',
|
|
||||||
'_seen'
|
|
||||||
];
|
|
||||||
defaults.map(add_field);
|
|
||||||
|
|
||||||
// add title field
|
|
||||||
if (this.meta.title_field) {
|
|
||||||
this.title_field = this.meta.title_field;
|
|
||||||
add_field(this.meta.title_field);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.meta.image_field) {
|
|
||||||
add_field(this.meta.image_field);
|
|
||||||
}
|
|
||||||
|
|
||||||
// enabled / disabled
|
|
||||||
if (frappe.meta.has_field(this.doctype, 'enabled')) { add_field('enabled'); }
|
|
||||||
if (frappe.meta.has_field(this.doctype, 'disabled')) { add_field('disabled'); }
|
|
||||||
|
|
||||||
// add workflow field (as priority)
|
|
||||||
this.workflow_state_fieldname = frappe.workflow.get_state_fieldname(this.doctype);
|
|
||||||
if (this.workflow_state_fieldname) {
|
|
||||||
if (!frappe.workflow.workflows[this.doctype]['override_status']) {
|
|
||||||
add_field(this.workflow_state_fieldname);
|
|
||||||
}
|
|
||||||
this.stats.push(this.workflow_state_fieldname);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.meta.fields.forEach(function (df, i) {
|
|
||||||
if (df.in_list_view && frappe.perm.has_perm(me.doctype, df.permlevel, 'read')) {
|
|
||||||
if (df.fieldtype == 'Image' && df.options) {
|
|
||||||
add_field(df.options);
|
|
||||||
} else {
|
|
||||||
add_field(df.fieldname);
|
|
||||||
}
|
|
||||||
// currency field for symbol (multi-currency)
|
|
||||||
if (df.fieldtype == 'Currency' && df.options) {
|
|
||||||
if (df.options.includes(':')) {
|
|
||||||
add_field(df.options.split(':')[1]);
|
|
||||||
} else {
|
|
||||||
add_field(df.options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// additional fields
|
|
||||||
if (this.settings.add_fields) {
|
|
||||||
this.settings.add_fields.forEach(add_field);
|
|
||||||
}
|
|
||||||
// kanban column fields
|
|
||||||
if (me.meta.__kanban_column_fields) {
|
|
||||||
me.meta.__kanban_column_fields.map(add_field);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set_columns: function () {
|
|
||||||
var me = this;
|
|
||||||
this.columns = [];
|
|
||||||
var name_column = {
|
|
||||||
colspan: this.settings.colwidths && this.settings.colwidths.subject || 6,
|
|
||||||
type: 'Subject',
|
|
||||||
title: 'Name'
|
|
||||||
};
|
|
||||||
if (this.meta.title_field) {
|
|
||||||
name_column.title = frappe.meta.get_docfield(this.doctype, this.meta.title_field).label;
|
|
||||||
}
|
|
||||||
this.columns.push(name_column);
|
|
||||||
|
|
||||||
if (frappe.has_indicator(this.doctype)) {
|
|
||||||
// indicator
|
|
||||||
this.columns.push({
|
|
||||||
colspan: this.settings.colwidths && this.settings.colwidths.indicator || 3,
|
|
||||||
type: 'Indicator',
|
|
||||||
title: 'Status'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// total_colspans
|
|
||||||
this.total_colspans = this.columns.reduce(function (total, curr) {
|
|
||||||
return total + curr.colspan;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// overridden
|
|
||||||
var overridden = (this.settings.add_columns || []).map(function (d) {
|
|
||||||
return d.content;
|
|
||||||
});
|
|
||||||
|
|
||||||
// custom fields in list_view
|
|
||||||
var docfields_in_list_view =
|
|
||||||
frappe.get_children('DocType', this.doctype, 'fields', { 'in_list_view': 1 })
|
|
||||||
.sort(function (a, b) {
|
|
||||||
return a.idx - b.idx
|
|
||||||
});
|
|
||||||
|
|
||||||
docfields_in_list_view.forEach(function (d) {
|
|
||||||
if (overridden.includes(d.fieldname) || d.fieldname === me.title_field) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (me.total_colspans < 12) {
|
|
||||||
me.add_column(d);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// additional columns
|
|
||||||
if (this.settings.add_columns) {
|
|
||||||
this.settings.add_columns.forEach(function (d) {
|
|
||||||
if (me.total_colspans < 12) {
|
|
||||||
if (typeof d === 'string') {
|
|
||||||
me.add_column(frappe.meta.get_docfield(me.doctype, d));
|
|
||||||
} else {
|
|
||||||
me.columns.push(d);
|
|
||||||
me.total_colspans += parseInt(d.colspan);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// distribute remaining columns
|
|
||||||
var empty_cols = flt(12 - this.total_colspans);
|
|
||||||
while (empty_cols > 0) {
|
|
||||||
this.columns = this.columns.map(function (col) {
|
|
||||||
if (empty_cols > 0) {
|
|
||||||
col.colspan = cint(col.colspan) + 1;
|
|
||||||
empty_cols = empty_cols - 1;
|
|
||||||
}
|
|
||||||
return col;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove duplicates
|
|
||||||
this.columns = this.columns.uniqBy(col => col.title);
|
|
||||||
|
|
||||||
// Remove TextEditor field columns
|
|
||||||
this.columns = this.columns.filter(col => col.fieldtype !== 'Text Editor');
|
|
||||||
|
|
||||||
// Remove color field
|
|
||||||
this.columns = this.columns.filter(col => col.fieldtype !== 'Color');
|
|
||||||
|
|
||||||
// Limit number of columns to 4
|
|
||||||
this.columns = this.columns.slice(0, 4);
|
|
||||||
},
|
|
||||||
add_column: function (df) {
|
|
||||||
// field width
|
|
||||||
var colspan = 3;
|
|
||||||
if (in_list(['Int', 'Percent'], df.fieldtype)) {
|
|
||||||
colspan = 2;
|
|
||||||
} else if (in_list(['Check', 'Image'], df.fieldtype)) {
|
|
||||||
colspan = 1;
|
|
||||||
} else if (in_list(['name', 'subject', 'title'], df.fieldname)) {
|
|
||||||
// subjects are longer
|
|
||||||
colspan = 4;
|
|
||||||
} else if (df.fieldtype == 'Text Editor' || df.fieldtype == 'Text') {
|
|
||||||
colspan = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (df.columns && df.columns > 0) {
|
|
||||||
colspan = df.columns;
|
|
||||||
} else if (this.settings.column_colspan && this.settings.column_colspan[df.fieldname]) {
|
|
||||||
colspan = this.settings.column_colspan[df.fieldname];
|
|
||||||
} else {
|
|
||||||
colspan = 2;
|
|
||||||
}
|
|
||||||
this.total_colspans += parseInt(colspan);
|
|
||||||
var col = {
|
|
||||||
colspan: colspan,
|
|
||||||
content: df.fieldname,
|
|
||||||
type: df.fieldtype,
|
|
||||||
df: df,
|
|
||||||
fieldtype: df.fieldtype,
|
|
||||||
fieldname: df.fieldname,
|
|
||||||
title: __(df.label)
|
|
||||||
};
|
|
||||||
if (this.settings.column_render && this.settings.column_render[df.fieldname]) {
|
|
||||||
col.render = this.settings.column_render[df.fieldname];
|
|
||||||
}
|
|
||||||
this.columns.push(col);
|
|
||||||
},
|
|
||||||
|
|
||||||
setup_filterable: function () {
|
|
||||||
var me = this;
|
|
||||||
|
|
||||||
this.list_view.wrapper &&
|
|
||||||
this.list_view.wrapper.on('click', '.result-list .filterable', function (e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
var filters = $(this).attr('data-filter').split('|');
|
|
||||||
var added = false;
|
|
||||||
|
|
||||||
filters.forEach(function (f) {
|
|
||||||
f = f.split(',');
|
|
||||||
if (f[2] === 'Today') {
|
|
||||||
f[2] = frappe.datetime.get_today();
|
|
||||||
} else if (f[2] == 'User') {
|
|
||||||
f[2] = frappe.session.user;
|
|
||||||
}
|
|
||||||
var new_filter = me.list_view.filter_list
|
|
||||||
.add_filter(me.doctype, f[0], f[1], f.slice(2).join(','));
|
|
||||||
|
|
||||||
if (new_filter) {
|
|
||||||
// set it to true if atleast 1 filter is added
|
|
||||||
added = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (added) {
|
|
||||||
me.list_view.refresh(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.list_view.wrapper &&
|
|
||||||
this.list_view.wrapper.on('click', '.list-item', function (e) {
|
|
||||||
// don't open in case of checkbox, like, filterable
|
|
||||||
if ($(e.target).hasClass('filterable')
|
|
||||||
|| $(e.target).hasClass('octicon-heart')
|
|
||||||
|| $(e.target).is(':checkbox')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var link = $(this).parent().find('a.list-id').get(0);
|
|
||||||
if ( link && link.href )
|
|
||||||
window.location.href = link.href;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
render_view: function (values) {
|
|
||||||
var me = this;
|
|
||||||
var $list_items = me.wrapper.find('.list-items');
|
|
||||||
|
|
||||||
if($list_items.length === 0) {
|
|
||||||
$list_items = $(`
|
|
||||||
<div class="list-items">
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
me.wrapper.append($list_items);
|
|
||||||
}
|
|
||||||
|
|
||||||
values.map(value => {
|
|
||||||
const $item = $(this.get_item_html(value));
|
|
||||||
const $item_container = $('<div class="list-item-container">').append($item);
|
|
||||||
|
|
||||||
$list_items.append($item_container);
|
|
||||||
|
|
||||||
if (this.settings.post_render_item) {
|
|
||||||
this.settings.post_render_item(this, $item_container, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.render_tags($item_container, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.render_count();
|
|
||||||
},
|
|
||||||
|
|
||||||
render_count: function() {
|
|
||||||
const $header_right = this.list_view.list_header.find('.list-item__content--activity');
|
|
||||||
const current_count = this.list_view.data.length;
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: 'frappe.desk.reportview.get',
|
|
||||||
args: {
|
|
||||||
doctype: this.doctype,
|
|
||||||
filters: this.list_view.get_filters_args(),
|
|
||||||
fields: ['count(`tab' + this.doctype + '`.name) as total_count']
|
|
||||||
}
|
|
||||||
}).then(r => {
|
|
||||||
const count = r.message.values[0][0] || current_count;
|
|
||||||
const str = __('{0} of {1}', [current_count, count]);
|
|
||||||
const $html = $(`<span>${str}</span>`);
|
|
||||||
|
|
||||||
$html.css({
|
|
||||||
marginRight: '10px'
|
|
||||||
})
|
|
||||||
$header_right.addClass('text-muted');
|
|
||||||
$header_right.html($html);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
// returns html for a data item,
|
|
||||||
// usually based on a template
|
|
||||||
get_item_html: function (data) {
|
|
||||||
var main = this.columns.map(column =>
|
|
||||||
frappe.render_template('list_item_main', {
|
|
||||||
data: data,
|
|
||||||
col: column,
|
|
||||||
value: data[column.fieldname],
|
|
||||||
formatters: this.settings.formatters,
|
|
||||||
subject: this.get_subject_html(data, true),
|
|
||||||
indicator: this.get_indicator_html(data),
|
|
||||||
})
|
|
||||||
).join("");
|
|
||||||
|
|
||||||
return frappe.render_template('list_item_row', {
|
|
||||||
data: data,
|
|
||||||
main: main,
|
|
||||||
settings: this.settings,
|
|
||||||
meta: this.meta,
|
|
||||||
indicator_dot: this.get_indicator_dot(data),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
get_header_html: function () {
|
|
||||||
var main = this.columns.map(column =>
|
|
||||||
frappe.render_template('list_item_main_head', {
|
|
||||||
col: column,
|
|
||||||
_checkbox: ((frappe.model.can_delete(this.doctype) || this.settings.selectable)
|
|
||||||
&& !this.no_delete)
|
|
||||||
})
|
|
||||||
).join("");
|
|
||||||
|
|
||||||
return frappe.render_template('list_item_row_head', { main: main, list: this });
|
|
||||||
},
|
|
||||||
|
|
||||||
render_tags: function (element, data) {
|
|
||||||
var me = this;
|
|
||||||
var tag_row = $(`<div class='tag-row'>
|
|
||||||
<div class='list-tag hidden-xs'></div>
|
|
||||||
<div class='clearfix'></div>
|
|
||||||
</div>`).appendTo(element);
|
|
||||||
|
|
||||||
if (!me.list_view.tags_shown) {
|
|
||||||
tag_row.addClass('hide');
|
|
||||||
}
|
|
||||||
|
|
||||||
// add tags
|
|
||||||
var tag_editor = new frappe.ui.TagEditor({
|
|
||||||
parent: tag_row.find('.list-tag'),
|
|
||||||
frm: {
|
|
||||||
doctype: this.doctype,
|
|
||||||
docname: data.name
|
|
||||||
},
|
|
||||||
list_sidebar: me.list_view.list_sidebar,
|
|
||||||
user_tags: data._user_tags,
|
|
||||||
on_change: function (user_tags) {
|
|
||||||
data._user_tags = user_tags;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
tag_editor.wrapper.on('click', '.tagit-label', function () {
|
|
||||||
me.list_view.set_filter('_user_tags', $(this).text());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
get_subject_html: function (data, without_workflow) {
|
|
||||||
data._without_workflow = without_workflow;
|
|
||||||
return frappe.render_template('list_item_subject', data);
|
|
||||||
},
|
|
||||||
|
|
||||||
get_indicator_html: function (doc) {
|
|
||||||
var indicator = frappe.get_indicator(doc, this.doctype);
|
|
||||||
if (indicator) {
|
|
||||||
return `<span class='indicator ${indicator[1]} filterable'
|
|
||||||
data-filter='${indicator[2]}'>
|
|
||||||
${__(indicator[0])}
|
|
||||||
<span>`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
get_indicator_dot: function (doc) {
|
|
||||||
var indicator = frappe.get_indicator(doc, this.doctype);
|
|
||||||
if (!indicator) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return `<span class='indicator ${indicator[1]}' title='${__(indicator[0])}'></span>`;
|
|
||||||
},
|
|
||||||
prepare_data: function (data) {
|
|
||||||
if (data.modified) {
|
|
||||||
this.prepare_when(data, data.modified);
|
|
||||||
}
|
|
||||||
|
|
||||||
// nulls as strings
|
|
||||||
for (var key in data) {
|
|
||||||
if (data[key] == null) {
|
|
||||||
data[key] = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data.doctype = this.doctype;
|
|
||||||
data._liked_by = JSON.parse(data._liked_by || '[]');
|
|
||||||
data._checkbox = (frappe.model.can_delete(this.doctype) || this.settings.selectable) && !this.no_delete
|
|
||||||
|
|
||||||
data._doctype_encoded = encodeURIComponent(data.doctype);
|
|
||||||
data._name = data.name.replace(/'/g, '\'');
|
|
||||||
data._name_encoded = encodeURIComponent(data.name);
|
|
||||||
data._submittable = frappe.model.is_submittable(this.doctype);
|
|
||||||
|
|
||||||
var title_field = this.meta.title_field || 'name';
|
|
||||||
data._title = strip_html(data[title_field] || data.name);
|
|
||||||
|
|
||||||
// check for duplicates
|
|
||||||
// add suffix like (1), (2) etc
|
|
||||||
if (data.name && this.values_map) {
|
|
||||||
if (this.values_map[data.name]!==undefined) {
|
|
||||||
if (this.values_map[data.name]===1) {
|
|
||||||
// update first row!
|
|
||||||
this.set_title_with_row_number(this.rows_map[data.name], 1);
|
|
||||||
}
|
|
||||||
this.values_map[data.name]++;
|
|
||||||
this.set_title_with_row_number(data, this.values_map[data.name]);
|
|
||||||
} else {
|
|
||||||
this.values_map[data.name] = 1;
|
|
||||||
this.rows_map[data.name] = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data._full_title = data._title;
|
|
||||||
|
|
||||||
data._workflow = null;
|
|
||||||
if (this.workflow_state_fieldname) {
|
|
||||||
data._workflow = {
|
|
||||||
fieldname: this.workflow_state_fieldname,
|
|
||||||
value: data[this.workflow_state_fieldname],
|
|
||||||
style: frappe.utils.guess_style(data[this.workflow_state_fieldname])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data._user = frappe.session.user;
|
|
||||||
|
|
||||||
if(!data._user_tags) data._user_tags = "";
|
|
||||||
|
|
||||||
data._tags = data._user_tags.split(',').filter(function (v) {
|
|
||||||
// filter falsy values
|
|
||||||
return v;
|
|
||||||
});
|
|
||||||
|
|
||||||
data.css_seen = '';
|
|
||||||
if (data._seen) {
|
|
||||||
var seen = JSON.parse(data._seen);
|
|
||||||
if (seen && in_list(seen, data._user)) {
|
|
||||||
data.css_seen = 'seen'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// whether to hide likes/comments/assignees
|
|
||||||
data._hide_activity = 0;
|
|
||||||
|
|
||||||
data._assign_list = JSON.parse(data._assign || '[]');
|
|
||||||
|
|
||||||
// prepare data in settings
|
|
||||||
if (this.settings.prepare_data)
|
|
||||||
this.settings.prepare_data(data);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
|
|
||||||
set_title_with_row_number: function (data, id) {
|
|
||||||
data._title = data._title + ` (${__("Row")} ${id})`;
|
|
||||||
data._full_title = data._title;
|
|
||||||
},
|
|
||||||
|
|
||||||
prepare_when: function (data, date_str) {
|
|
||||||
if (!date_str) date_str = data.modified;
|
|
||||||
// when
|
|
||||||
data.when = (frappe.datetime.str_to_user(date_str)).split(' ')[0];
|
|
||||||
var diff = frappe.datetime.get_diff(frappe.datetime.get_today(), date_str.split(' ')[0]);
|
|
||||||
if (diff === 0) {
|
|
||||||
data.when = comment_when(date_str);
|
|
||||||
}
|
|
||||||
if (diff === 1) {
|
|
||||||
data.when = __('Yesterday')
|
|
||||||
}
|
|
||||||
if (diff === 2) {
|
|
||||||
data.when = __('2 days ago')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// for views which require 3rd party libs
|
|
||||||
required_libs: null,
|
|
||||||
|
|
||||||
prepare_render_view: function () {
|
|
||||||
var me = this;
|
|
||||||
this._render_view = this.render_view;
|
|
||||||
|
|
||||||
var lib_exists = (typeof this.required_libs === 'string' && this.required_libs)
|
|
||||||
|| ($.isArray(this.required_libs) && this.required_libs.length);
|
|
||||||
|
|
||||||
this.render_view = function (values) {
|
|
||||||
me.values_map = {};
|
|
||||||
me.rows_map = {};
|
|
||||||
// prepare data before rendering view
|
|
||||||
values = values.map(me.prepare_data.bind(this));
|
|
||||||
// remove duplicates
|
|
||||||
// values = values.uniqBy(value => value.name);
|
|
||||||
|
|
||||||
if (lib_exists) {
|
|
||||||
me.load_lib(function () {
|
|
||||||
me._render_view(values);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
me._render_view(values);
|
|
||||||
}
|
|
||||||
}.bind(this);
|
|
||||||
},
|
|
||||||
|
|
||||||
load_lib: function (callback) {
|
|
||||||
frappe.require(this.required_libs, callback);
|
|
||||||
},
|
|
||||||
|
|
||||||
render_bar_graph: function (parent, data, field, label) {
|
|
||||||
var args = {
|
|
||||||
percent: data[field],
|
|
||||||
label: __(label)
|
|
||||||
}
|
|
||||||
$(parent).append(`<span class='progress' style='width: 100 %; float: left; margin: 5px 0px;'> \
|
|
||||||
<span class='progress- bar' title='${args.percent}% ${args.label}' \
|
|
||||||
style='width: ${args.percent}%;'></span>\
|
|
||||||
</span>`);
|
|
||||||
},
|
|
||||||
render_icon: function (parent, icon_class, label) {
|
|
||||||
var icon_html = `<i class='${icon_class}' title='${__(label) || ''}'></i>`;
|
|
||||||
$(parent).append(icon_html);
|
|
||||||
},
|
|
||||||
make_no_result: function () {
|
|
||||||
var new_button = frappe.boot.user.can_create.includes(this.doctype)
|
|
||||||
? (`<p><button class='btn btn-primary btn-sm'
|
|
||||||
list_view_doc='${this.doctype}'>
|
|
||||||
${__('Create a new {0}', [__(this.doctype)])}
|
|
||||||
</button></p>`)
|
|
||||||
: '';
|
|
||||||
var no_result_message =
|
|
||||||
`<div class='msg-box no-border'>
|
|
||||||
<p>${__('No {0} found', [__(this.doctype)])}</p>
|
|
||||||
${new_button}
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
return no_result_message;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -258,6 +258,9 @@ frappe.views.ListSidebar = class ListSidebar {
|
||||||
|
|
||||||
get_stats() {
|
get_stats() {
|
||||||
var me = this;
|
var me = this;
|
||||||
|
if (this.list_view.list_view_settings && this.list_view.list_view_settings.disable_sidebar_stats) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'frappe.desk.reportview.get_sidebar_stats',
|
method: 'frappe.desk.reportview.get_sidebar_stats',
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,11 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.patch_refresh_and_load_lib();
|
this.patch_refresh_and_load_lib();
|
||||||
|
return this.get_list_view_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
get_list_view_settings() {
|
||||||
|
return frappe.call("frappe.desk.listview.get_list_settings", {doctype: this.doctype}).then(doc => this.list_view_settings = doc.message || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
on_sort_change(sort_by, sort_order) {
|
on_sort_change(sort_by, sort_order) {
|
||||||
|
|
@ -289,7 +294,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
||||||
}
|
}
|
||||||
|
|
||||||
freeze() {
|
freeze() {
|
||||||
this.$result.find('.list-count').html(`<span>${__('Refreshing')}...</span>`);
|
if (this.list_view_settings && !this.list_view_settings.disable_count) {
|
||||||
|
this.$result.find('.list-count').html(`<span>${__('Refreshing')}...</span>`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get_args() {
|
get_args() {
|
||||||
|
|
@ -375,10 +382,11 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
||||||
}
|
}
|
||||||
|
|
||||||
render_count() {
|
render_count() {
|
||||||
this.get_count_str()
|
if (!this.list_view_settings.disable_count) {
|
||||||
.then(str => {
|
this.get_count_str().then(str => {
|
||||||
this.$result.find('.list-count').html(`<span>${str}</span>`);
|
this.$result.find('.list-count').html(`<span>${str}</span>`);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_tags() {
|
render_tags() {
|
||||||
|
|
@ -824,6 +832,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_realtime_updates() {
|
setup_realtime_updates() {
|
||||||
|
if (this.list_view_settings.disable_auto_refresh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
frappe.realtime.on('list_update', data => {
|
frappe.realtime.on('list_update', data => {
|
||||||
if (this.filter_area.is_being_edited()) {
|
if (this.filter_area.is_being_edited()) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -1026,9 +1037,33 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frappe.user.has_role('System Manager')) {
|
||||||
|
items.push({
|
||||||
|
label: __('Settings'),
|
||||||
|
action: () => this.show_list_settings(),
|
||||||
|
standard: true
|
||||||
|
});
|
||||||
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
show_list_settings() {
|
||||||
|
frappe.model.with_doctype("List View Setting", () => {
|
||||||
|
let d = new frappe.ui.Dialog({
|
||||||
|
title: __("Settings"),
|
||||||
|
fields: frappe.get_meta("List View Setting").fields
|
||||||
|
});
|
||||||
|
d.set_values(this.list_view_settings);
|
||||||
|
d.show();
|
||||||
|
d.set_primary_action(__('Save'), () => {
|
||||||
|
let values = d.get_values();
|
||||||
|
frappe.call("frappe.desk.listview.set_list_settings", {doctype: this.doctype, values: values});
|
||||||
|
Object.assign(this.list_view_settings, values);
|
||||||
|
d.hide();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get_actions_menu_items() {
|
get_actions_menu_items() {
|
||||||
const doctype = this.doctype;
|
const doctype = this.doctype;
|
||||||
const actions_menu_items = [];
|
const actions_menu_items = [];
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ $.extend(frappe.model, {
|
||||||
for(var fid=0;fid<docfields.length;fid++) {
|
for(var fid=0;fid<docfields.length;fid++) {
|
||||||
var f = docfields[fid];
|
var f = docfields[fid];
|
||||||
if(!in_list(frappe.model.no_value_type, f.fieldtype) && doc[f.fieldname]==null) {
|
if(!in_list(frappe.model.no_value_type, f.fieldtype) && doc[f.fieldname]==null) {
|
||||||
var v = frappe.model.get_default_value(f, doc, parent_doc);
|
var v = !f.depends_on || doc[f.depends_on] ? frappe.model.get_default_value(f, doc, parent_doc) : null;
|
||||||
if(v) {
|
if(v) {
|
||||||
if(in_list(["Int", "Check"], f.fieldtype))
|
if(in_list(["Int", "Check"], f.fieldtype))
|
||||||
v = cint(v);
|
v = cint(v);
|
||||||
|
|
|
||||||
|
|
@ -1,539 +0,0 @@
|
||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
// MIT License. See license.txt
|
|
||||||
|
|
||||||
// new re-re-factored Listing object
|
|
||||||
// now called BaseList
|
|
||||||
//
|
|
||||||
// opts:
|
|
||||||
// parent
|
|
||||||
|
|
||||||
// method (method to call on server)
|
|
||||||
// args (additional args to method)
|
|
||||||
// get_args (method to return args as dict)
|
|
||||||
|
|
||||||
// show_filters [false]
|
|
||||||
// doctype
|
|
||||||
// filter_fields (if given, this list is rendered, else built from doctype)
|
|
||||||
|
|
||||||
// query or get_query (will be deprecated)
|
|
||||||
// query_max
|
|
||||||
// buttons_in_frame
|
|
||||||
|
|
||||||
// no_result_message ("No result")
|
|
||||||
|
|
||||||
// page_length (20)
|
|
||||||
// hide_refresh (False)
|
|
||||||
// no_toolbar
|
|
||||||
// new_doctype
|
|
||||||
// [function] render_row(parent, data)
|
|
||||||
// [function] onrun
|
|
||||||
// no_loading (no ajax indicator)
|
|
||||||
|
|
||||||
frappe.provide('frappe.ui');
|
|
||||||
|
|
||||||
frappe.ui.BaseList = Class.extend({
|
|
||||||
init: function (opts) {
|
|
||||||
this.opts = opts || {};
|
|
||||||
this.set_defaults();
|
|
||||||
if (opts) {
|
|
||||||
this.make();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set_defaults: function () {
|
|
||||||
this.page_length = 20;
|
|
||||||
this.start = 0;
|
|
||||||
this.data = [];
|
|
||||||
},
|
|
||||||
make: function (opts) {
|
|
||||||
if (opts) {
|
|
||||||
this.opts = opts;
|
|
||||||
}
|
|
||||||
this.prepare_opts();
|
|
||||||
|
|
||||||
$.extend(this, this.opts);
|
|
||||||
|
|
||||||
// make dom
|
|
||||||
this.wrapper = $(frappe.render_template('listing', this.opts));
|
|
||||||
this.parent.append(this.wrapper);
|
|
||||||
|
|
||||||
this.set_events();
|
|
||||||
|
|
||||||
if (this.page) {
|
|
||||||
this.wrapper.find('.list-toolbar-wrapper').hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.show_filters) {
|
|
||||||
this.make_filters();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
prepare_opts: function () {
|
|
||||||
if (this.opts.new_doctype) {
|
|
||||||
if (!frappe.boot.user.can_create.includes(this.opts.new_doctype)) {
|
|
||||||
this.opts.new_doctype = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!this.opts.no_result_message) {
|
|
||||||
this.opts.no_result_message = __('Nothing to show');
|
|
||||||
}
|
|
||||||
if (!this.opts.page_length) {
|
|
||||||
this.opts.page_length = this.user_settings && this.user_settings.limit || 20;
|
|
||||||
}
|
|
||||||
this.opts._more = __('More');
|
|
||||||
},
|
|
||||||
add_button: function (label, click, icon) {
|
|
||||||
if (this.page) {
|
|
||||||
return this.page.add_menu_item(label, click, icon)
|
|
||||||
} else {
|
|
||||||
this.wrapper.find('.list-toolbar-wrapper').removeClass('hide');
|
|
||||||
return $('<button class="btn btn-default"></button>')
|
|
||||||
.appendTo(this.wrapper.find('.list-toolbar'))
|
|
||||||
.html((icon ? ('<i class="' + icon + '"></i> ') : '') + label)
|
|
||||||
.click(click);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set_events: function () {
|
|
||||||
var me = this;
|
|
||||||
|
|
||||||
// next page
|
|
||||||
this.wrapper.find('.btn-more').click(function () {
|
|
||||||
me.run(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.wrapper.find(".btn-group-paging").on('click', '.btn', function () {
|
|
||||||
me.page_length = cint($(this).attr("data-value"));
|
|
||||||
|
|
||||||
me.wrapper.find(".btn-group-paging .btn-info").removeClass("btn-info");
|
|
||||||
$(this).addClass("btn-info");
|
|
||||||
|
|
||||||
// always reset when changing list page length
|
|
||||||
me.run();
|
|
||||||
});
|
|
||||||
|
|
||||||
// select the correct page length
|
|
||||||
if (this.opts.page_length !== 20) {
|
|
||||||
this.wrapper.find(".btn-group-paging .btn-info").removeClass("btn-info");
|
|
||||||
this.wrapper
|
|
||||||
.find(".btn-group-paging .btn[data-value='" + this.opts.page_length + "']")
|
|
||||||
.addClass('btn-info');
|
|
||||||
}
|
|
||||||
|
|
||||||
// title
|
|
||||||
if (this.title) {
|
|
||||||
this.wrapper.find('h3').html(this.title).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// new
|
|
||||||
this.set_primary_action();
|
|
||||||
|
|
||||||
if (me.no_toolbar || me.hide_toolbar) {
|
|
||||||
me.wrapper.find('.list-toolbar-wrapper').hide();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
set_primary_action: function () {
|
|
||||||
var me = this;
|
|
||||||
if (this.new_doctype) {
|
|
||||||
this.page.set_primary_action(
|
|
||||||
__("New"),
|
|
||||||
me.make_new_doc.bind(me, me.new_doctype),
|
|
||||||
"octicon octicon-plus"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.page.clear_primary_action();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
make_new_doc: function (doctype) {
|
|
||||||
var me = this;
|
|
||||||
frappe.model.with_doctype(doctype, function () {
|
|
||||||
if (me.custom_new_doc) {
|
|
||||||
me.custom_new_doc(doctype);
|
|
||||||
} else {
|
|
||||||
if (me.filter_list) {
|
|
||||||
frappe.route_options = {};
|
|
||||||
me.filter_list.get_filters().forEach(function (f, i) {
|
|
||||||
if (f[2] === "=" && !frappe.model.std_fields_list.includes(f[1])) {
|
|
||||||
frappe.route_options[f[1]] = f[3];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
frappe.new_doc(doctype, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
make_filters: function () {
|
|
||||||
this.make_standard_filters();
|
|
||||||
|
|
||||||
this.filter_list = new frappe.ui.FilterList({
|
|
||||||
base_list: this,
|
|
||||||
parent: this.wrapper.find('.list-filters').show(),
|
|
||||||
doctype: this.doctype,
|
|
||||||
filter_fields: this.filter_fields,
|
|
||||||
default_filters: this.default_filters || []
|
|
||||||
});
|
|
||||||
// default filter for submittable doctype
|
|
||||||
if (frappe.model.is_submittable(this.doctype)) {
|
|
||||||
this.filter_list.add_filter(this.doctype, "docstatus", "!=", 2);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
make_standard_filters: function() {
|
|
||||||
var me = this;
|
|
||||||
if (this.standard_filters_added) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.meta) {
|
|
||||||
var filter_count = 1;
|
|
||||||
if(this.is_list_view) {
|
|
||||||
$(`<span class="octicon octicon-search text-muted small"></span>`)
|
|
||||||
.prependTo(this.page.page_form);
|
|
||||||
}
|
|
||||||
this.page.add_field({
|
|
||||||
fieldtype: 'Data',
|
|
||||||
label: 'ID',
|
|
||||||
condition: 'like',
|
|
||||||
fieldname: 'name',
|
|
||||||
onchange: () => { me.refresh(true); }
|
|
||||||
});
|
|
||||||
|
|
||||||
this.meta.fields.forEach(function(df, i) {
|
|
||||||
if(df.in_standard_filter && !frappe.model.no_value_type.includes(df.fieldtype)) {
|
|
||||||
let options = df.options;
|
|
||||||
let condition = '=';
|
|
||||||
let fieldtype = df.fieldtype;
|
|
||||||
if (['Text', 'Small Text', 'Text Editor', 'Data'].includes(fieldtype)) {
|
|
||||||
fieldtype = 'Data';
|
|
||||||
condition = 'like';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (df.fieldtype === "Select" && df.options) {
|
|
||||||
options = df.options.split("\n");
|
|
||||||
if(options.length > 0 && options[0] != "") {
|
|
||||||
options.unshift("");
|
|
||||||
options = options.join("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (df.fieldtype === 'Data' && df.options) {
|
|
||||||
// don't format email / number in filters
|
|
||||||
options = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
let f = me.page.add_field({
|
|
||||||
fieldtype: fieldtype,
|
|
||||||
label: __(df.label),
|
|
||||||
options: options,
|
|
||||||
fieldname: df.fieldname,
|
|
||||||
condition: condition,
|
|
||||||
onchange: () => {me.refresh(true);}
|
|
||||||
});
|
|
||||||
filter_count ++;
|
|
||||||
if (filter_count > 3) {
|
|
||||||
$(f.wrapper).addClass('hidden-sm').addClass('hidden-xs');
|
|
||||||
}
|
|
||||||
if (filter_count > 5) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.standard_filters_added = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
update_standard_filters: function(filters) {
|
|
||||||
let me = this;
|
|
||||||
for(let key in this.page.fields_dict) {
|
|
||||||
let field = this.page.fields_dict[key];
|
|
||||||
let value = field.get_value();
|
|
||||||
if (value) {
|
|
||||||
if (field.df.condition==='like' && !value.includes('%')) {
|
|
||||||
value = '%' + value + '%';
|
|
||||||
}
|
|
||||||
filters.push([
|
|
||||||
me.doctype,
|
|
||||||
field.df.fieldname,
|
|
||||||
field.df.condition || '=',
|
|
||||||
value
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
clear: function () {
|
|
||||||
this.data = [];
|
|
||||||
this.wrapper.find('.list-select-all').prop('checked', false);
|
|
||||||
this.wrapper.find('.result-list').empty();
|
|
||||||
this.wrapper.find('.result').show();
|
|
||||||
this.wrapper.find('.no-result').hide();
|
|
||||||
this.start = 0;
|
|
||||||
this.onreset && this.onreset();
|
|
||||||
},
|
|
||||||
|
|
||||||
set_filters_from_route_options: function ({clear_filters=true} = {}) {
|
|
||||||
var me = this;
|
|
||||||
if(this.filter_list && clear_filters) {
|
|
||||||
this.filter_list.clear_filters();
|
|
||||||
}
|
|
||||||
|
|
||||||
for(var field in frappe.route_options) {
|
|
||||||
var value = frappe.route_options[field];
|
|
||||||
var doctype = null;
|
|
||||||
|
|
||||||
// if `Child DocType.fieldname`
|
|
||||||
if (field.includes(".")) {
|
|
||||||
doctype = field.split(".")[0];
|
|
||||||
field = field.split(".")[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the table in which the key exists
|
|
||||||
// for example the filter could be {"item_code": "X"}
|
|
||||||
// where item_code is in the child table.
|
|
||||||
|
|
||||||
// we can search all tables for mapping the doctype
|
|
||||||
if (!doctype) {
|
|
||||||
doctype = frappe.meta.get_doctype_for_field(me.doctype, field);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doctype && me.filter_list) {
|
|
||||||
if ($.isArray(value)) {
|
|
||||||
me.filter_list.add_filter(doctype, field, value[0], value[1]);
|
|
||||||
} else {
|
|
||||||
me.filter_list.add_filter(doctype, field, "=", value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
frappe.route_options = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
run: function(more) {
|
|
||||||
setTimeout(() => this._run(more), 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
_run: function (more) {
|
|
||||||
var me = this;
|
|
||||||
if (!more) {
|
|
||||||
this.start = 0;
|
|
||||||
this.onreset && this.onreset();
|
|
||||||
}
|
|
||||||
|
|
||||||
var args = this.get_call_args();
|
|
||||||
this.save_user_settings_locally(args);
|
|
||||||
|
|
||||||
// user_settings are saved by db_query.py when dirty
|
|
||||||
$.extend(args, {
|
|
||||||
user_settings: frappe.model.user_settings[this.doctype]
|
|
||||||
});
|
|
||||||
|
|
||||||
return frappe.call({
|
|
||||||
method: this.opts.method || 'frappe.desk.query_builder.runquery',
|
|
||||||
freeze: this.opts.freeze !== undefined ? this.opts.freeze : true,
|
|
||||||
args: args,
|
|
||||||
callback: function (r) {
|
|
||||||
me.dirty = false;
|
|
||||||
me.render_results(r);
|
|
||||||
},
|
|
||||||
no_spinner: this.opts.no_loading
|
|
||||||
});
|
|
||||||
},
|
|
||||||
save_user_settings_locally: function (args) {
|
|
||||||
if (this.opts.save_user_settings && this.doctype && !this.docname) {
|
|
||||||
// save list settings locally
|
|
||||||
var user_settings = frappe.model.user_settings[this.doctype];
|
|
||||||
var different = false;
|
|
||||||
|
|
||||||
if (!user_settings) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!frappe.utils.arrays_equal(args.filters, user_settings.filters)) {
|
|
||||||
// settings are dirty if filters change
|
|
||||||
user_settings.filters = args.filters;
|
|
||||||
different = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user_settings.order_by !== args.order_by) {
|
|
||||||
user_settings.order_by = args.order_by;
|
|
||||||
different = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user_settings.limit !== args.limit_page_length) {
|
|
||||||
user_settings.limit = args.limit_page_length || 20
|
|
||||||
different = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// save fields in list settings
|
|
||||||
if (args.save_user_settings_fields) {
|
|
||||||
user_settings.fields = args.fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (different) {
|
|
||||||
user_settings.updated_on = moment().toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
get_call_args: function () {
|
|
||||||
// load query
|
|
||||||
if (!this.method) {
|
|
||||||
var query = this.get_query && this.get_query() || this.query;
|
|
||||||
query = this.add_limits(query);
|
|
||||||
var args = {
|
|
||||||
query_max: this.query_max,
|
|
||||||
as_dict: 1
|
|
||||||
}
|
|
||||||
args.simple_query = query;
|
|
||||||
} else {
|
|
||||||
var args = {
|
|
||||||
start: this.start,
|
|
||||||
page_length: this.page_length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// append user-defined arguments
|
|
||||||
if (this.args)
|
|
||||||
$.extend(args, this.args)
|
|
||||||
|
|
||||||
if (this.get_args) {
|
|
||||||
$.extend(args, this.get_args());
|
|
||||||
}
|
|
||||||
return args;
|
|
||||||
},
|
|
||||||
render_results: function (r) {
|
|
||||||
if (this.start === 0)
|
|
||||||
this.clear();
|
|
||||||
|
|
||||||
this.wrapper.find('.btn-more, .list-loading').hide();
|
|
||||||
|
|
||||||
var values = [];
|
|
||||||
|
|
||||||
if (r.message) {
|
|
||||||
values = this.get_values_from_response(r.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
var show_results = true;
|
|
||||||
if(this.show_no_result) {
|
|
||||||
if($.isFunction(this.show_no_result)) {
|
|
||||||
show_results = !this.show_no_result()
|
|
||||||
} else {
|
|
||||||
show_results = !this.show_no_result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// render result view when
|
|
||||||
// length > 0 OR
|
|
||||||
// explicitly set by flag
|
|
||||||
if (values.length || show_results) {
|
|
||||||
this.data = this.data.concat(values);
|
|
||||||
this.render_view(values);
|
|
||||||
this.update_paging(values);
|
|
||||||
} else if (this.start === 0) {
|
|
||||||
// show no result message
|
|
||||||
this.wrapper.find('.result').hide();
|
|
||||||
|
|
||||||
var msg = '';
|
|
||||||
var no_result_message = this.no_result_message;
|
|
||||||
if(no_result_message && $.isFunction(no_result_message)) {
|
|
||||||
msg = no_result_message();
|
|
||||||
} else if(typeof no_result_message === 'string') {
|
|
||||||
msg = no_result_message;
|
|
||||||
} else {
|
|
||||||
msg = __('No Results')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.wrapper.find('.no-result').html(msg).show();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.wrapper.find('.list-paging-area')
|
|
||||||
.toggle(values.length > 0|| this.start > 0);
|
|
||||||
|
|
||||||
// callbacks
|
|
||||||
if (this.onrun) this.onrun();
|
|
||||||
if (this.callback) this.callback(r);
|
|
||||||
this.wrapper.trigger("render-complete");
|
|
||||||
},
|
|
||||||
|
|
||||||
get_values_from_response: function (data) {
|
|
||||||
// make dictionaries from keys and values
|
|
||||||
if (data.keys && $.isArray(data.keys)) {
|
|
||||||
return frappe.utils.dict(data.keys, data.values);
|
|
||||||
} else {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render_view: function (values) {
|
|
||||||
// override this method in derived class
|
|
||||||
},
|
|
||||||
|
|
||||||
update_paging: function (values) {
|
|
||||||
if (values.length >= this.page_length) {
|
|
||||||
this.wrapper.find('.btn-more').show();
|
|
||||||
this.start += this.page_length;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
refresh: function () {
|
|
||||||
this.run();
|
|
||||||
},
|
|
||||||
add_limits: function (query) {
|
|
||||||
return query + ' LIMIT ' + this.start + ',' + (this.page_length + 1);
|
|
||||||
},
|
|
||||||
set_filter: function (fieldname, label, no_run, no_duplicate) {
|
|
||||||
var filter = this.filter_list.get_filter(fieldname);
|
|
||||||
if (filter) {
|
|
||||||
var value = cstr(filter.field.get_value());
|
|
||||||
if (value.includes(label)) {
|
|
||||||
// already set
|
|
||||||
return false
|
|
||||||
|
|
||||||
} else if (no_duplicate) {
|
|
||||||
filter.set_values(this.doctype, fieldname, "=", label);
|
|
||||||
} else {
|
|
||||||
// second filter set for this field
|
|
||||||
if (fieldname == '_user_tags' || fieldname == "_liked_by") {
|
|
||||||
// and for tags
|
|
||||||
this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label + '%');
|
|
||||||
} else {
|
|
||||||
// or for rest using "in"
|
|
||||||
filter.set_values(this.doctype, fieldname, 'in', value + ', ' + label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// no filter for this item,
|
|
||||||
// setup one
|
|
||||||
if (['_user_tags', '_comments', '_assign', '_liked_by'].includes(fieldname)) {
|
|
||||||
this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label + '%');
|
|
||||||
} else {
|
|
||||||
this.filter_list.add_filter(this.doctype, fieldname, '=', label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!no_run)
|
|
||||||
this.run();
|
|
||||||
},
|
|
||||||
init_user_settings: function () {
|
|
||||||
this.user_settings = frappe.model.user_settings[this.doctype] || {};
|
|
||||||
},
|
|
||||||
call_for_selected_items: function (method, args) {
|
|
||||||
var me = this;
|
|
||||||
args.names = this.get_checked_items().map(function (item) {
|
|
||||||
return item.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: method,
|
|
||||||
args: args,
|
|
||||||
freeze: true,
|
|
||||||
callback: function (r) {
|
|
||||||
if (!r.exc) {
|
|
||||||
if (me.list_header) {
|
|
||||||
me.list_header.find(".list-select-all").prop("checked", false);
|
|
||||||
}
|
|
||||||
me.refresh(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -77,13 +77,9 @@ frappe.ui.Tags = class {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTag(label) {
|
removeTag(label) {
|
||||||
|
label = frappe.utils.xss_sanitise(label);
|
||||||
if(this.tagsList.includes(label)) {
|
if(this.tagsList.includes(label)) {
|
||||||
let $tag = this.$ul.find(`.frappe-tag[data-tag-label="${label}"]`);
|
|
||||||
|
|
||||||
// Just don't remove tag, but also the li DOM.
|
|
||||||
$tag.parent('.tags-list-item').remove();
|
|
||||||
this.tagsList.splice(this.tagsList.indexOf(label), 1);
|
this.tagsList.splice(this.tagsList.indexOf(label), 1);
|
||||||
|
|
||||||
this.onTagRemove && this.onTagRemove(label);
|
this.onTagRemove && this.onTagRemove(label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -119,6 +115,7 @@ frappe.ui.Tags = class {
|
||||||
|
|
||||||
$removeTag.on("click", () => {
|
$removeTag.on("click", () => {
|
||||||
this.removeTag($removeTag.attr('data-tag-label'));
|
this.removeTag($removeTag.attr('data-tag-label'));
|
||||||
|
$removeTag.closest('.tags-list-item').remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
if(this.onTagClick) {
|
if(this.onTagClick) {
|
||||||
|
|
|
||||||
|
|
@ -43,16 +43,18 @@ frappe.views.pageview = {
|
||||||
name = (frappe.boot ? frappe.boot.home_page : window.page_name);
|
name = (frappe.boot ? frappe.boot.home_page : window.page_name);
|
||||||
|
|
||||||
if(name === "desktop") {
|
if(name === "desktop") {
|
||||||
let page = frappe.container.add_page('desktop');
|
if(!frappe.pages.desktop) {
|
||||||
|
let page = frappe.container.add_page('desktop');
|
||||||
|
let container = $('<div class="container"></div>').appendTo(page);
|
||||||
|
container = $('<div></div>').appendTo(container);
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: container[0],
|
||||||
|
render: h => h(Desktop)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
frappe.container.change_to('desktop');
|
frappe.container.change_to('desktop');
|
||||||
|
|
||||||
let container = $('<div class="container"></div>').appendTo(page);
|
|
||||||
container = $('<div></div>').appendTo(container);
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el: container[0],
|
|
||||||
render: h => h(Desktop)
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
||||||
this.secondary_action = {
|
this.secondary_action = {
|
||||||
label: __('Refresh'),
|
label: __('Refresh'),
|
||||||
action: () => {
|
action: () => {
|
||||||
if(this.execution_time > 2) {
|
this.setup_progress_bar();
|
||||||
this.setup_progress_bar();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -171,8 +168,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
||||||
|
|
||||||
setup_progress_bar() {
|
setup_progress_bar() {
|
||||||
let seconds_elapsed = 0;
|
let seconds_elapsed = 0;
|
||||||
const execution_time = this.report_settings.execution_time < 10
|
const execution_time = this.report_settings.execution_time || 0;
|
||||||
? 10 : this.report_settings.execution_time;
|
|
||||||
|
if (execution_time < 5) return;
|
||||||
|
|
||||||
this.interval = setInterval(function() {
|
this.interval = setInterval(function() {
|
||||||
seconds_elapsed += 1;
|
seconds_elapsed += 1;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
@import "variables.less";
|
||||||
.awesomplete {
|
.awesomplete {
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,11 @@ hr {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-unstyled {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* auto email report */
|
/* auto email report */
|
||||||
.report-title {
|
.report-title {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,11 @@ h6.uppercase, .h6.uppercase {
|
||||||
|
|
||||||
.timeline-items {
|
.timeline-items {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
.timeline-item-content {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline {
|
.timeline {
|
||||||
|
|
@ -943,3 +948,7 @@ body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.followed-by-label{
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ql-editor {
|
.ql-editor {
|
||||||
|
font-family: @font-stack;
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
h1, h2, h3, h4, h5 {
|
h1, h2, h3, h4, h5 {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
margin-bottom: 0.25em;
|
margin-bottom: 0.25em;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import traceback
|
||||||
import frappe
|
import frappe
|
||||||
import sqlparse
|
import sqlparse
|
||||||
|
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
RECORDER_INTERCEPT_FLAG = "recorder-intercept"
|
RECORDER_INTERCEPT_FLAG = "recorder-intercept"
|
||||||
RECORDER_REQUEST_SPARSE_HASH = "recorder-requests-sparse"
|
RECORDER_REQUEST_SPARSE_HASH = "recorder-requests-sparse"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.desk.form.document_follow import follow_document
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|
@ -41,6 +42,8 @@ def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=No
|
||||||
doc.save(ignore_permissions=True)
|
doc.save(ignore_permissions=True)
|
||||||
notify_assignment(user, doctype, name, description=None, notify=notify)
|
notify_assignment(user, doctype, name, description=None, notify=notify)
|
||||||
|
|
||||||
|
follow_document(doctype, name, user)
|
||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
def remove(doctype, name, user, flags=None):
|
def remove(doctype, name, user, flags=None):
|
||||||
|
|
|
||||||
88
frappe/templates/emails/document_follow.html
Normal file
88
frappe/templates/emails/document_follow.html
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<h3>Document Follow Notification</h3>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% for doc in docinfo%}
|
||||||
|
<table class="panel-header" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr height="10"></tr>
|
||||||
|
<tr>
|
||||||
|
<td width="15"></td>
|
||||||
|
<td>
|
||||||
|
<div class="text-medium text-muted">
|
||||||
|
<span><a href="{{doc.reference_url}}">{{ doc.reference_doctype }}: {{doc.reference_docname }}</a></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td width="15"></td>
|
||||||
|
</tr>
|
||||||
|
<tr height="10"></tr>
|
||||||
|
</table>
|
||||||
|
<table class="panel-body" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr height="10"></tr>
|
||||||
|
<tr>
|
||||||
|
<td width="15"></td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<ul class="list-unstyled" style="line-height: 1.7">
|
||||||
|
{% for data in timeline %}
|
||||||
|
{% if (data.doctype == doc.reference_doctype and data.doc_name == doc.reference_docname) %}
|
||||||
|
{% if data.type == "comment" %}
|
||||||
|
<li>
|
||||||
|
<span style ='color:#8d99a6!important'>
|
||||||
|
{{data.data.time}}:
|
||||||
|
</span>
|
||||||
|
<b>"{{data.data.comment}}"</b>
|
||||||
|
{{data.data.by}}
|
||||||
|
</li>
|
||||||
|
{% elif data.type == "row added" %}
|
||||||
|
<li>
|
||||||
|
<span style ='color:#8d99a6!important'>
|
||||||
|
{{data.data.time}}:
|
||||||
|
</span>
|
||||||
|
Row Added to Table Field
|
||||||
|
<b>{{data.data.to}}</b>
|
||||||
|
By:
|
||||||
|
<b>{{data.by}}</b>
|
||||||
|
</li>
|
||||||
|
{% elif data.type == "field changed" %}
|
||||||
|
<li>
|
||||||
|
<span style ='color:#8d99a6!important'>
|
||||||
|
{{data.data.time}}:
|
||||||
|
</span>Field:
|
||||||
|
<b>"{{data.data.field}}"</b>
|
||||||
|
changed from
|
||||||
|
<b>"{{data.data.from}}"</b>
|
||||||
|
to
|
||||||
|
<b>"{{data.data.to}}"</b>
|
||||||
|
By:
|
||||||
|
<b>{{data.by}}</b>
|
||||||
|
</li>
|
||||||
|
{% elif data.type == "row changed" %}
|
||||||
|
<li>
|
||||||
|
<span style ='color:#8d99a6!important'>
|
||||||
|
{{data.data.time}}:
|
||||||
|
</span>
|
||||||
|
Table Field:
|
||||||
|
<b>"{{data.data.table_field}}"</b>
|
||||||
|
Row# {{data.data.row}} Field:
|
||||||
|
<b>"{{data.data.field}}"</b>
|
||||||
|
changed from
|
||||||
|
<b>"{{data.data.from}}" </b>
|
||||||
|
to <b>"{{data.data.to}}"</b>
|
||||||
|
By:
|
||||||
|
<b>{{data.by}}</b>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td width="15"></td>
|
||||||
|
</tr>
|
||||||
|
<tr height="10"></tr>
|
||||||
|
</table>
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr height="20"></tr>
|
||||||
|
</table>
|
||||||
|
{% endfor %}
|
||||||
|
|
@ -339,12 +339,12 @@ class TestReportview(unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
def test_is_set_is_not_set(self):
|
def test_is_set_is_not_set(self):
|
||||||
res = DatabaseQuery("DocType").execute(filters={"autoname": ["is", "not set"]})
|
res = DatabaseQuery('DocType').execute(filters={'autoname': ['is', 'not set']})
|
||||||
self.assertTrue({'name': 'Integration Request'} in res)
|
self.assertTrue({'name': 'Integration Request'} in res)
|
||||||
self.assertTrue({'name': 'User'} in res)
|
self.assertTrue({'name': 'User'} in res)
|
||||||
self.assertFalse({'name': 'Blogger'} in res)
|
self.assertFalse({'name': 'Blogger'} in res)
|
||||||
|
|
||||||
res = DatabaseQuery("DocType").execute(filters={"autoname": ["is", "set"]})
|
res = DatabaseQuery('DocType').execute(filters={'autoname': ['is', 'set']})
|
||||||
self.assertTrue({'name': 'DocField'} in res)
|
self.assertTrue({'name': 'DocField'} in res)
|
||||||
self.assertTrue({'name': 'Prepared Report'} in res)
|
self.assertTrue({'name': 'Prepared Report'} in res)
|
||||||
self.assertFalse({'name': 'Property Setter'} in res)
|
self.assertFalse({'name': 'Property Setter'} in res)
|
||||||
|
|
|
||||||
53
frappe/tests/test_listview.py
Normal file
53
frappe/tests/test_listview.py
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# MIT License. See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import frappe
|
||||||
|
import json
|
||||||
|
|
||||||
|
from frappe.desk.listview import get_list_settings, set_list_settings
|
||||||
|
|
||||||
|
class TestListView(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
if frappe.db.exists("List View Setting", "DocType"):
|
||||||
|
frappe.delete_doc("List View Setting", "DocType")
|
||||||
|
|
||||||
|
def test_get_list_settings_without_settings(self):
|
||||||
|
self.assertIsNone(get_list_settings("DocType"), None)
|
||||||
|
|
||||||
|
def test_get_list_settings_with_default_settings(self):
|
||||||
|
frappe.get_doc({"doctype": "List View Setting", "name": "DocType"}).insert()
|
||||||
|
settings = get_list_settings("DocType")
|
||||||
|
self.assertIsNotNone(settings)
|
||||||
|
|
||||||
|
self.assertEqual(settings.disable_auto_refresh, 0)
|
||||||
|
self.assertEqual(settings.disable_count, 0)
|
||||||
|
self.assertEqual(settings.disable_sidebar_stats, 0)
|
||||||
|
|
||||||
|
def test_get_list_settings_with_non_default_settings(self):
|
||||||
|
frappe.get_doc({"doctype": "List View Setting", "name": "DocType", "disable_count": 1}).insert()
|
||||||
|
settings = get_list_settings("DocType")
|
||||||
|
self.assertIsNotNone(settings)
|
||||||
|
|
||||||
|
self.assertEqual(settings.disable_auto_refresh, 0)
|
||||||
|
self.assertEqual(settings.disable_count, 1)
|
||||||
|
self.assertEqual(settings.disable_sidebar_stats, 0)
|
||||||
|
|
||||||
|
def test_set_list_settings_without_settings(self):
|
||||||
|
set_list_settings("DocType", json.dumps({}))
|
||||||
|
settings = frappe.get_doc("List View Setting","DocType")
|
||||||
|
|
||||||
|
self.assertEqual(settings.disable_auto_refresh, 0)
|
||||||
|
self.assertEqual(settings.disable_count, 0)
|
||||||
|
self.assertEqual(settings.disable_sidebar_stats, 0)
|
||||||
|
|
||||||
|
def test_set_list_settings_with_existing_settings(self):
|
||||||
|
frappe.get_doc({"doctype": "List View Setting", "name": "DocType", "disable_count": 1}).insert()
|
||||||
|
set_list_settings("DocType", json.dumps({"disable_count": 0, "disable_auto_refresh": 1}))
|
||||||
|
settings = frappe.get_doc("List View Setting","DocType")
|
||||||
|
|
||||||
|
self.assertEqual(settings.disable_auto_refresh, 1)
|
||||||
|
self.assertEqual(settings.disable_count, 0)
|
||||||
|
self.assertEqual(settings.disable_sidebar_stats, 0)
|
||||||
|
|
||||||
|
|
@ -10,6 +10,12 @@ def set_request(**kwargs):
|
||||||
builder = EnvironBuilder(**kwargs)
|
builder = EnvironBuilder(**kwargs)
|
||||||
frappe.local.request = Request(builder.get_environ())
|
frappe.local.request = Request(builder.get_environ())
|
||||||
|
|
||||||
|
def get_html_for_route(route):
|
||||||
|
set_request(method='GET', path=route)
|
||||||
|
response = render.render()
|
||||||
|
html = frappe.safe_decode(response.get_data())
|
||||||
|
return html
|
||||||
|
|
||||||
class TestWebsite(unittest.TestCase):
|
class TestWebsite(unittest.TestCase):
|
||||||
|
|
||||||
def test_page_load(self):
|
def test_page_load(self):
|
||||||
|
|
|
||||||
|
|
@ -564,7 +564,9 @@ def parse_json(val):
|
||||||
Parses json if string else return
|
Parses json if string else return
|
||||||
"""
|
"""
|
||||||
if isinstance(val, string_types):
|
if isinstance(val, string_types):
|
||||||
return json.loads(val)
|
val = json.loads(val)
|
||||||
|
if isinstance(val, dict):
|
||||||
|
val = frappe._dict(val)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def cast_fieldtype(fieldtype, value):
|
def cast_fieldtype(fieldtype, value):
|
||||||
|
|
|
||||||
|
|
@ -326,7 +326,6 @@ def ceil(s):
|
||||||
|
|
||||||
def cstr(s, encoding='utf-8'):
|
def cstr(s, encoding='utf-8'):
|
||||||
return frappe.as_unicode(s, encoding)
|
return frappe.as_unicode(s, encoding)
|
||||||
|
|
||||||
def rounded(num, precision=0):
|
def rounded(num, precision=0):
|
||||||
"""round method for round halfs to nearest even algorithm aka banker's rounding - compatible with python3"""
|
"""round method for round halfs to nearest even algorithm aka banker's rounding - compatible with python3"""
|
||||||
precision = cint(precision)
|
precision = cint(precision)
|
||||||
|
|
|
||||||
|
|
@ -205,9 +205,9 @@ def get_frame_locals():
|
||||||
frames = []
|
frames = []
|
||||||
if traceback:
|
if traceback:
|
||||||
frames = inspect.getinnerframes(traceback, context=0)
|
frames = inspect.getinnerframes(traceback, context=0)
|
||||||
_locals = ['Locals (most recent call last):']
|
_locals = ['Locals (most recent call last):']
|
||||||
for frame, filename, lineno, function, __, __ in frames:
|
for frame, filename, lineno, function, __, __ in frames:
|
||||||
if '/apps/' in filename:
|
if '/apps/' in filename:
|
||||||
_locals.append('File "{}", line {}, in {}\n{}'.format(filename, lineno, function, json.dumps(frame.f_locals, default=str, indent=4)))
|
_locals.append('File "{}", line {}, in {}\n{}'.format(filename, lineno, function, json.dumps(frame.f_locals, default=str, indent=4)))
|
||||||
|
|
||||||
return '\n'.join(_locals)
|
return '\n'.join(_locals)
|
||||||
|
|
|
||||||
453
frappe/utils/file_manager.py
Normal file
453
frappe/utils/file_manager.py
Normal file
|
|
@ -0,0 +1,453 @@
|
||||||
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# MIT License. See license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
import os, base64, re, json
|
||||||
|
import hashlib
|
||||||
|
import mimetypes
|
||||||
|
import io
|
||||||
|
from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint
|
||||||
|
from frappe import _
|
||||||
|
from frappe import conf
|
||||||
|
from copy import copy
|
||||||
|
from six.moves.urllib.parse import unquote
|
||||||
|
from six import text_type, PY2, string_types
|
||||||
|
|
||||||
|
|
||||||
|
class MaxFileSizeReachedError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_url(file_data_name):
|
||||||
|
data = frappe.db.get_value("File", file_data_name, ["file_name", "file_url"], as_dict=True)
|
||||||
|
return data.file_url or data.file_name
|
||||||
|
|
||||||
|
|
||||||
|
def upload():
|
||||||
|
# get record details
|
||||||
|
dt = frappe.form_dict.doctype
|
||||||
|
dn = frappe.form_dict.docname
|
||||||
|
file_url = frappe.form_dict.file_url
|
||||||
|
filename = frappe.form_dict.filename
|
||||||
|
frappe.form_dict.is_private = cint(frappe.form_dict.is_private)
|
||||||
|
|
||||||
|
if not filename and not file_url:
|
||||||
|
frappe.msgprint(_("Please select a file or url"),
|
||||||
|
raise_exception=True)
|
||||||
|
|
||||||
|
file_doc = get_file_doc()
|
||||||
|
|
||||||
|
comment = {}
|
||||||
|
if dt and dn:
|
||||||
|
comment = frappe.get_doc(dt, dn).add_comment("Attachment",
|
||||||
|
_("added {0}").format("<a href='{file_url}' target='_blank'>{file_name}</a>{icon}".format(**{
|
||||||
|
"icon": ' <i class="fa fa-lock text-warning"></i>' \
|
||||||
|
if file_doc.is_private else "",
|
||||||
|
"file_url": file_doc.file_url.replace("#", "%23") \
|
||||||
|
if file_doc.file_name else file_doc.file_url,
|
||||||
|
"file_name": file_doc.file_name or file_doc.file_url
|
||||||
|
})))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": file_doc.name,
|
||||||
|
"file_name": file_doc.file_name,
|
||||||
|
"file_url": file_doc.file_url,
|
||||||
|
"is_private": file_doc.is_private,
|
||||||
|
"comment": comment.as_dict() if comment else {}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_file_doc(dt=None, dn=None, folder=None, is_private=None, df=None):
|
||||||
|
'''returns File object (Document) from given parameters or form_dict'''
|
||||||
|
r = frappe.form_dict
|
||||||
|
|
||||||
|
if dt is None: dt = r.doctype
|
||||||
|
if dn is None: dn = r.docname
|
||||||
|
if df is None: df = r.docfield
|
||||||
|
if folder is None: folder = r.folder
|
||||||
|
if is_private is None: is_private = r.is_private
|
||||||
|
|
||||||
|
if r.filedata:
|
||||||
|
file_doc = save_uploaded(dt, dn, folder, is_private, df)
|
||||||
|
|
||||||
|
elif r.file_url:
|
||||||
|
file_doc = save_url(r.file_url, r.filename, dt, dn, folder, is_private, df)
|
||||||
|
|
||||||
|
return file_doc
|
||||||
|
|
||||||
|
def save_uploaded(dt, dn, folder, is_private, df=None):
|
||||||
|
fname, content = get_uploaded_content()
|
||||||
|
if content:
|
||||||
|
return save_file(fname, content, dt, dn, folder, is_private=is_private, df=df);
|
||||||
|
else:
|
||||||
|
raise Exception
|
||||||
|
|
||||||
|
def save_url(file_url, filename, dt, dn, folder, is_private, df=None):
|
||||||
|
# if not (file_url.startswith("http://") or file_url.startswith("https://")):
|
||||||
|
# frappe.msgprint("URL must start with 'http://' or 'https://'")
|
||||||
|
# return None, None
|
||||||
|
|
||||||
|
file_url = unquote(file_url)
|
||||||
|
file_size = frappe.form_dict.file_size
|
||||||
|
|
||||||
|
f = frappe.get_doc({
|
||||||
|
"doctype": "File",
|
||||||
|
"file_url": file_url,
|
||||||
|
"file_name": filename,
|
||||||
|
"attached_to_doctype": dt,
|
||||||
|
"attached_to_name": dn,
|
||||||
|
"attached_to_field": df,
|
||||||
|
"folder": folder,
|
||||||
|
"file_size": file_size,
|
||||||
|
"is_private": is_private
|
||||||
|
})
|
||||||
|
f.flags.ignore_permissions = True
|
||||||
|
try:
|
||||||
|
f.insert()
|
||||||
|
except frappe.DuplicateEntryError:
|
||||||
|
return frappe.get_doc("File", f.duplicate_entry)
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def get_uploaded_content():
|
||||||
|
# should not be unicode when reading a file, hence using frappe.form
|
||||||
|
if 'filedata' in frappe.form_dict:
|
||||||
|
if "," in frappe.form_dict.filedata:
|
||||||
|
frappe.form_dict.filedata = frappe.form_dict.filedata.rsplit(",", 1)[1]
|
||||||
|
frappe.uploaded_content = base64.b64decode(frappe.form_dict.filedata)
|
||||||
|
frappe.uploaded_filename = frappe.form_dict.filename
|
||||||
|
return frappe.uploaded_filename, frappe.uploaded_content
|
||||||
|
else:
|
||||||
|
frappe.msgprint(_('No file attached'))
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def save_file(fname, content, dt, dn, folder=None, decode=False, is_private=0, df=None):
|
||||||
|
if decode:
|
||||||
|
if isinstance(content, text_type):
|
||||||
|
content = content.encode("utf-8")
|
||||||
|
|
||||||
|
if b"," in content:
|
||||||
|
content = content.split(b",")[1]
|
||||||
|
content = base64.b64decode(content)
|
||||||
|
|
||||||
|
file_size = check_max_file_size(content)
|
||||||
|
content_hash = get_content_hash(content)
|
||||||
|
content_type = mimetypes.guess_type(fname)[0]
|
||||||
|
fname = get_file_name(fname, content_hash[-6:])
|
||||||
|
file_data = get_file_data_from_hash(content_hash, is_private=is_private)
|
||||||
|
if not file_data:
|
||||||
|
call_hook_method("before_write_file", file_size=file_size)
|
||||||
|
|
||||||
|
write_file_method = get_hook_method('write_file', fallback=save_file_on_filesystem)
|
||||||
|
file_data = write_file_method(fname, content, content_type=content_type, is_private=is_private)
|
||||||
|
file_data = copy(file_data)
|
||||||
|
|
||||||
|
file_data.update({
|
||||||
|
"doctype": "File",
|
||||||
|
"attached_to_doctype": dt,
|
||||||
|
"attached_to_name": dn,
|
||||||
|
"attached_to_field": df,
|
||||||
|
"folder": folder,
|
||||||
|
"file_size": file_size,
|
||||||
|
"content_hash": content_hash,
|
||||||
|
"is_private": is_private
|
||||||
|
})
|
||||||
|
|
||||||
|
f = frappe.get_doc(file_data)
|
||||||
|
f.flags.ignore_permissions = True
|
||||||
|
try:
|
||||||
|
f.insert()
|
||||||
|
except frappe.DuplicateEntryError:
|
||||||
|
return frappe.get_doc("File", f.duplicate_entry)
|
||||||
|
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_data_from_hash(content_hash, is_private=0):
|
||||||
|
for name in frappe.db.sql_list("select name from `tabFile` where content_hash=%s and is_private=%s", (content_hash, is_private)):
|
||||||
|
b = frappe.get_doc('File', name)
|
||||||
|
return {k: b.get(k) for k in frappe.get_hooks()['write_file_keys']}
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def save_file_on_filesystem(fname, content, content_type=None, is_private=0):
|
||||||
|
fpath = write_file(content, fname, is_private)
|
||||||
|
|
||||||
|
if is_private:
|
||||||
|
file_url = "/private/files/{0}".format(fname)
|
||||||
|
else:
|
||||||
|
file_url = "/files/{0}".format(fname)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'file_name': os.path.basename(fpath),
|
||||||
|
'file_url': file_url
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_max_file_size():
|
||||||
|
return conf.get('max_file_size') or 10485760
|
||||||
|
|
||||||
|
|
||||||
|
def check_max_file_size(content):
|
||||||
|
max_file_size = get_max_file_size()
|
||||||
|
file_size = len(content)
|
||||||
|
|
||||||
|
if file_size > max_file_size:
|
||||||
|
frappe.msgprint(_("File size exceeded the maximum allowed size of {0} MB").format(
|
||||||
|
max_file_size / 1048576),
|
||||||
|
raise_exception=MaxFileSizeReachedError)
|
||||||
|
|
||||||
|
return file_size
|
||||||
|
|
||||||
|
|
||||||
|
def write_file(content, fname, is_private=0):
|
||||||
|
"""write file to disk with a random name (to compare)"""
|
||||||
|
file_path = get_files_path(is_private=is_private)
|
||||||
|
|
||||||
|
# create directory (if not exists)
|
||||||
|
frappe.create_folder(file_path)
|
||||||
|
# write the file
|
||||||
|
if isinstance(content, text_type):
|
||||||
|
content = content.encode()
|
||||||
|
with open(os.path.join(file_path.encode('utf-8'), fname.encode('utf-8')), 'wb+') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
return get_files_path(fname, is_private=is_private)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_all(dt, dn, from_delete=False):
|
||||||
|
"""remove all files in a transaction"""
|
||||||
|
try:
|
||||||
|
for fid in frappe.db.sql_list("""select name from `tabFile` where
|
||||||
|
attached_to_doctype=%s and attached_to_name=%s""", (dt, dn)):
|
||||||
|
remove_file(fid, dt, dn, from_delete)
|
||||||
|
except Exception as e:
|
||||||
|
if e.args[0]!=1054: raise # (temp till for patched)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_file_by_url(file_url, doctype=None, name=None):
|
||||||
|
if doctype and name:
|
||||||
|
fid = frappe.db.get_value("File", {"file_url": file_url,
|
||||||
|
"attached_to_doctype": doctype, "attached_to_name": name})
|
||||||
|
else:
|
||||||
|
fid = frappe.db.get_value("File", {"file_url": file_url})
|
||||||
|
|
||||||
|
if fid:
|
||||||
|
return remove_file(fid)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_file(fid, attached_to_doctype=None, attached_to_name=None, from_delete=False):
|
||||||
|
"""Remove file and File entry"""
|
||||||
|
file_name = None
|
||||||
|
if not (attached_to_doctype and attached_to_name):
|
||||||
|
attached = frappe.db.get_value("File", fid,
|
||||||
|
["attached_to_doctype", "attached_to_name", "file_name"])
|
||||||
|
if attached:
|
||||||
|
attached_to_doctype, attached_to_name, file_name = attached
|
||||||
|
|
||||||
|
ignore_permissions, comment = False, None
|
||||||
|
if attached_to_doctype and attached_to_name and not from_delete:
|
||||||
|
doc = frappe.get_doc(attached_to_doctype, attached_to_name)
|
||||||
|
ignore_permissions = doc.has_permission("write") or False
|
||||||
|
if frappe.flags.in_web_form:
|
||||||
|
ignore_permissions = True
|
||||||
|
if not file_name:
|
||||||
|
file_name = frappe.db.get_value("File", fid, "file_name")
|
||||||
|
comment = doc.add_comment("Attachment Removed", _("Removed {0}").format(file_name))
|
||||||
|
|
||||||
|
frappe.delete_doc("File", fid, ignore_permissions=ignore_permissions)
|
||||||
|
|
||||||
|
return comment
|
||||||
|
|
||||||
|
|
||||||
|
def delete_file_data_content(doc, only_thumbnail=False):
|
||||||
|
method = get_hook_method('delete_file_data_content', fallback=delete_file_from_filesystem)
|
||||||
|
method(doc, only_thumbnail=only_thumbnail)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_file_from_filesystem(doc, only_thumbnail=False):
|
||||||
|
"""Delete file, thumbnail from File document"""
|
||||||
|
if only_thumbnail:
|
||||||
|
delete_file(doc.thumbnail_url)
|
||||||
|
else:
|
||||||
|
delete_file(doc.file_url)
|
||||||
|
delete_file(doc.thumbnail_url)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_file(path):
|
||||||
|
"""Delete file from `public folder`"""
|
||||||
|
if path:
|
||||||
|
if ".." in path.split("/"):
|
||||||
|
frappe.msgprint(_("It is risky to delete this file: {0}. Please contact your System Manager.").format(path))
|
||||||
|
|
||||||
|
parts = os.path.split(path.strip("/"))
|
||||||
|
if parts[0]=="files":
|
||||||
|
path = frappe.utils.get_site_path("public", "files", parts[-1])
|
||||||
|
|
||||||
|
else:
|
||||||
|
path = frappe.utils.get_site_path("private", "files", parts[-1])
|
||||||
|
|
||||||
|
path = encode(path)
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.remove(path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_file(fname):
|
||||||
|
"""Returns [`file_name`, `content`] for given file name `fname`"""
|
||||||
|
file_path = get_file_path(fname)
|
||||||
|
|
||||||
|
# read the file
|
||||||
|
if PY2:
|
||||||
|
with open(encode(file_path)) as f:
|
||||||
|
content = f.read()
|
||||||
|
else:
|
||||||
|
with io.open(encode(file_path), mode='rb') as f:
|
||||||
|
content = f.read()
|
||||||
|
try:
|
||||||
|
# for plain text files
|
||||||
|
content = content.decode()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# for .png, .jpg, etc
|
||||||
|
pass
|
||||||
|
|
||||||
|
return [file_path.rsplit("/", 1)[-1], content]
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_path(file_name):
|
||||||
|
"""Returns file path from given file name"""
|
||||||
|
f = frappe.db.sql("""select file_url from `tabFile`
|
||||||
|
where name=%s or file_name=%s""", (file_name, file_name))
|
||||||
|
if f:
|
||||||
|
file_name = f[0][0]
|
||||||
|
|
||||||
|
file_path = file_name
|
||||||
|
|
||||||
|
if "/" not in file_path:
|
||||||
|
file_path = "/files/" + file_path
|
||||||
|
|
||||||
|
if file_path.startswith("/private/files/"):
|
||||||
|
file_path = get_files_path(*file_path.split("/private/files/", 1)[1].split("/"), is_private=1)
|
||||||
|
|
||||||
|
elif file_path.startswith("/files/"):
|
||||||
|
file_path = get_files_path(*file_path.split("/files/", 1)[1].split("/"))
|
||||||
|
|
||||||
|
else:
|
||||||
|
frappe.throw(_("There is some problem with the file url: {0}").format(file_path))
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
|
def get_content_hash(content):
|
||||||
|
if isinstance(content, text_type):
|
||||||
|
content = content.encode()
|
||||||
|
return hashlib.md5(content).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_name(fname, optional_suffix):
|
||||||
|
# convert to unicode
|
||||||
|
fname = cstr(fname)
|
||||||
|
|
||||||
|
n_records = frappe.db.sql("select name from `tabFile` where file_name=%s", fname)
|
||||||
|
if len(n_records) > 0 or os.path.exists(encode(get_files_path(fname))):
|
||||||
|
f = fname.rsplit('.', 1)
|
||||||
|
if len(f) == 1:
|
||||||
|
partial, extn = f[0], ""
|
||||||
|
else:
|
||||||
|
partial, extn = f[0], "." + f[1]
|
||||||
|
return '{partial}{suffix}{extn}'.format(partial=partial, extn=extn, suffix=optional_suffix)
|
||||||
|
return fname
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def download_file(file_url):
|
||||||
|
"""
|
||||||
|
Download file using token and REST API. Valid session or
|
||||||
|
token is required to download private files.
|
||||||
|
|
||||||
|
Method : GET
|
||||||
|
Endpoint : frappe.utils.file_manager.download_file
|
||||||
|
URL Params : file_name = /path/to/file relative to site path
|
||||||
|
"""
|
||||||
|
file_doc = frappe.get_doc("File", {"file_url":file_url})
|
||||||
|
file_doc.check_permission("read")
|
||||||
|
path = os.path.join(get_files_path(), os.path.basename(file_url))
|
||||||
|
|
||||||
|
with open(path, "rb") as fileobj:
|
||||||
|
filedata = fileobj.read()
|
||||||
|
frappe.local.response.filename = os.path.basename(file_url)
|
||||||
|
frappe.local.response.filecontent = filedata
|
||||||
|
frappe.local.response.type = "download"
|
||||||
|
|
||||||
|
def extract_images_from_doc(doc, fieldname):
|
||||||
|
content = doc.get(fieldname)
|
||||||
|
content = extract_images_from_html(doc, content)
|
||||||
|
if frappe.flags.has_dataurl:
|
||||||
|
doc.set(fieldname, content)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_images_from_html(doc, content):
|
||||||
|
frappe.flags.has_dataurl = False
|
||||||
|
|
||||||
|
def _save_file(match):
|
||||||
|
data = match.group(1)
|
||||||
|
data = data.split("data:")[1]
|
||||||
|
headers, content = data.split(",")
|
||||||
|
|
||||||
|
if "filename=" in headers:
|
||||||
|
filename = headers.split("filename=")[-1]
|
||||||
|
|
||||||
|
# decode filename
|
||||||
|
if not isinstance(filename, text_type):
|
||||||
|
filename = text_type(filename, 'utf-8')
|
||||||
|
else:
|
||||||
|
mtype = headers.split(";")[0]
|
||||||
|
filename = get_random_filename(content_type=mtype)
|
||||||
|
|
||||||
|
doctype = doc.parenttype if doc.parent else doc.doctype
|
||||||
|
name = doc.parent or doc.name
|
||||||
|
|
||||||
|
# TODO fix this
|
||||||
|
file_url = save_file(filename, content, doctype, name, decode=True).get("file_url")
|
||||||
|
if not frappe.flags.has_dataurl:
|
||||||
|
frappe.flags.has_dataurl = True
|
||||||
|
|
||||||
|
return '<img src="{file_url}"'.format(file_url=file_url)
|
||||||
|
|
||||||
|
if content:
|
||||||
|
content = re.sub('<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, content)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def get_random_filename(extn=None, content_type=None):
|
||||||
|
if extn:
|
||||||
|
if not extn.startswith("."):
|
||||||
|
extn = "." + extn
|
||||||
|
|
||||||
|
elif content_type:
|
||||||
|
extn = mimetypes.guess_extension(content_type)
|
||||||
|
|
||||||
|
return random_string(7) + (extn or "")
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def validate_filename(filename):
|
||||||
|
from frappe.utils import now_datetime
|
||||||
|
timestamp = now_datetime().strftime(" %Y-%m-%d %H:%M:%S")
|
||||||
|
fname = get_file_name(filename, timestamp)
|
||||||
|
return fname
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def add_attachments(doctype, name, attachments):
|
||||||
|
'''Add attachments to the given DocType'''
|
||||||
|
if isinstance(attachments, string_types):
|
||||||
|
attachments = json.loads(attachments)
|
||||||
|
# loop through attachments
|
||||||
|
files =[]
|
||||||
|
for a in attachments:
|
||||||
|
if isinstance(a, string_types):
|
||||||
|
attach = frappe.db.get_value("File", {"name":a}, ["file_name", "file_url", "is_private"], as_dict=1)
|
||||||
|
# save attachments to new doc
|
||||||
|
f = save_url(attach.file_url, attach.file_name, doctype, name, "Home/Attachments", attach.is_private)
|
||||||
|
files.append(f)
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
@ -104,21 +104,24 @@ def before_tests():
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
|
|
||||||
# complete setup if missing
|
# complete setup if missing
|
||||||
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
|
|
||||||
if not int(frappe.db.get_single_value('System Settings', 'setup_complete') or 0):
|
if not int(frappe.db.get_single_value('System Settings', 'setup_complete') or 0):
|
||||||
setup_complete({
|
complete_setup_wizard()
|
||||||
"language" :"English",
|
|
||||||
"email" :"test@erpnext.com",
|
|
||||||
"full_name" :"Test User",
|
|
||||||
"password" :"test",
|
|
||||||
"country" :"United States",
|
|
||||||
"timezone" :"America/New_York",
|
|
||||||
"currency" :"USD"
|
|
||||||
})
|
|
||||||
|
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
|
|
||||||
|
def complete_setup_wizard():
|
||||||
|
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
|
||||||
|
setup_complete({
|
||||||
|
"language" :"English",
|
||||||
|
"email" :"test@erpnext.com",
|
||||||
|
"full_name" :"Test User",
|
||||||
|
"password" :"test",
|
||||||
|
"country" :"United States",
|
||||||
|
"timezone" :"America/New_York",
|
||||||
|
"currency" :"USD"
|
||||||
|
})
|
||||||
|
|
||||||
def import_country_and_currency():
|
def import_country_and_currency():
|
||||||
from frappe.geo.country_info import get_all
|
from frappe.geo.country_info import get_all
|
||||||
from frappe.utils import update_progress_bar
|
from frappe.utils import update_progress_bar
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,17 @@ def handle_html(data):
|
||||||
obj = HTML2Text()
|
obj = HTML2Text()
|
||||||
obj.ignore_links = True
|
obj.ignore_links = True
|
||||||
obj.body_width = 0
|
obj.body_width = 0
|
||||||
value = obj.handle(h)
|
|
||||||
|
try:
|
||||||
|
value = obj.handle(h)
|
||||||
|
except Exception:
|
||||||
|
# unable to parse html, send it raw
|
||||||
|
return value
|
||||||
|
|
||||||
value = ", ".join(value.split(' \n'))
|
value = ", ".join(value.split(' \n'))
|
||||||
value = " ".join(value.split('\n'))
|
value = " ".join(value.split('\n'))
|
||||||
value = ", ".join(value.split('# '))
|
value = ", ".join(value.split('# '))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def read_xlsx_file_from_attached_file(file_url=None, fcontent=None, filepath=None):
|
def read_xlsx_file_from_attached_file(file_url=None, fcontent=None, filepath=None):
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
{% block title %}{{ _(title) }}{% endblock %}
|
{% block title %}{{ _(title) }}{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<h1>{{ _(title) }}</h1>
|
<h2>{{ _(title) }}</h2>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -27,11 +27,11 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_list %}
|
{% if is_list %}
|
||||||
<div style="padding-bottom: 15px;">
|
<div class="text-right mt-2">
|
||||||
<a href="/{{ pathname }}{{ delimeter }}new=1{{ params_from_form_dict}}" class="btn btn-primary btn-new btn-sm">
|
<a href="/{{ pathname }}{{ delimeter }}new=1{{ params_from_form_dict}}" class="btn btn-primary btn-new">
|
||||||
{{ _("New") }}
|
{{ _("New") }}
|
||||||
</a>
|
</a>
|
||||||
<button class="btn btn-danger btn-delete btn-sm">
|
<button class="btn btn-danger btn-delete">
|
||||||
{{ _("Delete") }}
|
{{ _("Delete") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,12 +46,14 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page_content %}
|
{% block page_content %}
|
||||||
<div class="introduction">
|
{% if introduction_text %}
|
||||||
{% if introduction_text %}
|
<div class="introduction">
|
||||||
<p class="text-muted">{{ _(introduction_text) }}</p>
|
<p class="text-muted">{{ _(introduction_text) }}</p>
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
{% else %}
|
||||||
<div class="form-message hidden"></div>
|
<hr style="margin-top: 0;">
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-message hide"></div>
|
||||||
|
|
||||||
{% if _login_required %}
|
{% if _login_required %}
|
||||||
<div class="login-required">
|
<div class="login-required">
|
||||||
|
|
@ -151,8 +153,8 @@
|
||||||
<!-- save/next button -->
|
<!-- save/next button -->
|
||||||
{% if (loop.index == layout|len or frappe.form_dict.new) %}
|
{% if (loop.index == layout|len or frappe.form_dict.new) %}
|
||||||
{% if not read_only %}
|
{% if not read_only %}
|
||||||
<button type="submit" class="btn btn-primary btn-sm btn-form-submit">
|
<button type="submit" class="btn btn-primary btn-sm btn-form-submit footer-button">
|
||||||
{{ _(button_label or "Save") }}</button>
|
{{ _("Save") }}</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif layout|len > 1 %}
|
{% elif layout|len > 1 %}
|
||||||
<button class="btn btn-primary btn-sm btn-change-section"
|
<button class="btn btn-primary btn-sm btn-change-section"
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export default function make_datatable(container, doctype) {
|
||||||
.css({cursor: 'pointer'})
|
.css({cursor: 'pointer'})
|
||||||
.click((e) => {
|
.click((e) => {
|
||||||
if ($(e.target).is('[type=checkbox]')) return
|
if ($(e.target).is('[type=checkbox]')) return
|
||||||
window.location.href = window.location.href + '?name=' + data[i].name;
|
window.location.href = window.location.origin + window.location.pathname + '?name=' + data[i].name;
|
||||||
});
|
});
|
||||||
for (let fieldname of colnames) {
|
for (let fieldname of colnames) {
|
||||||
let val = data[i][fieldname];
|
let val = data[i][fieldname];
|
||||||
|
|
@ -116,14 +116,13 @@ export default function make_datatable(container, doctype) {
|
||||||
args: { doctype },
|
args: { doctype },
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
const docfields = r.message;
|
const docfields = r.message;
|
||||||
|
var data = frappe.utils.get_query_params();
|
||||||
|
data.doctype = doctype;
|
||||||
|
data.fields = docfields.map(df => df.fieldname);
|
||||||
|
data.web_form_name = window.web_form_settings.web_form_name;
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'frappe.www.list.get_list_data',
|
method: 'frappe.www.list.get_list_data',
|
||||||
args: {
|
args: data,
|
||||||
doctype,
|
|
||||||
fields: docfields.map(df => df.fieldname),
|
|
||||||
web_form_name: window.web_form_settings.web_form_name
|
|
||||||
},
|
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
const data = r.message || [];
|
const data = r.message || [];
|
||||||
make_table(docfields, data);
|
make_table(docfields, data);
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,11 @@ frappe.ready(function() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$('body').css('display', 'block');
|
$('body').css('display', 'block');
|
||||||
|
|
||||||
|
// remove footer save button if form height is less than window height
|
||||||
|
if($('.webform-wrapper').height() < window.innerHeight) {
|
||||||
|
$(".footer-button").addClass("hide");
|
||||||
|
}
|
||||||
|
|
||||||
if (frappe.init_client_script) {
|
if (frappe.init_client_script) {
|
||||||
frappe.init_client_script();
|
frappe.init_client_script();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,35 +13,34 @@ $.extend(frappe, {
|
||||||
lang: 'en'
|
lang: 'en'
|
||||||
},
|
},
|
||||||
_assets_loaded: [],
|
_assets_loaded: [],
|
||||||
require: function(url, callback) {
|
require: async function(links, callback) {
|
||||||
|
if (typeof (links) === 'string') {
|
||||||
let async = false;
|
links = [links];
|
||||||
if (callback) {
|
|
||||||
async = true;
|
|
||||||
}
|
}
|
||||||
|
for (let link of links) {
|
||||||
if(frappe._assets_loaded.indexOf(url)!==-1) {
|
await this.add_asset_to_head(link);
|
||||||
callback && callback();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
callback && callback();
|
||||||
return $.ajax({
|
},
|
||||||
url: url,
|
add_asset_to_head(link) {
|
||||||
async: async,
|
return new Promise(resolve => {
|
||||||
dataType: "text",
|
if (frappe._assets_loaded.includes(link)) return resolve();
|
||||||
success: function(data) {
|
let el;
|
||||||
var el;
|
if(link.split('.').pop() === 'js') {
|
||||||
if(url.split(".").splice(-1) == "js") {
|
el = document.createElement('script');
|
||||||
el = document.createElement('script');
|
el.type = 'text/javascript';
|
||||||
} else {
|
el.src = link;
|
||||||
el = document.createElement('style');
|
} else {
|
||||||
}
|
el = document.createElement('link');
|
||||||
el.appendChild(document.createTextNode(data));
|
el.type = 'text/css';
|
||||||
document.getElementsByTagName('head')[0].appendChild(el);
|
el.rel = 'stylesheet';
|
||||||
frappe._assets_loaded.push(url);
|
el.href = link;
|
||||||
|
|
||||||
callback && callback();
|
|
||||||
}
|
}
|
||||||
|
document.getElementsByTagName('head')[0].appendChild(el);
|
||||||
|
el.onload = () => {
|
||||||
|
frappe._assets_loaded.push(link);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
hide_message: function() {
|
hide_message: function() {
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ def get(doctype, txt=None, limit_start=0, limit=20, pathname=None, **kwargs):
|
||||||
}
|
}
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_list_data(doctype, txt=None, limit_start=0, limit=20, **kwargs):
|
def get_list_data(doctype, txt=None, limit_start=0, fields=None, cmd=None, limit=20, **kwargs):
|
||||||
"""Returns processed HTML page for a standard listing."""
|
"""Returns processed HTML page for a standard listing."""
|
||||||
limit_start = cint(limit_start)
|
limit_start = cint(limit_start)
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Add table
Reference in a new issue