Merge branch 'develop' of https://github.com/frappe/frappe into force_listview_columns

This commit is contained in:
Himanshu Warekar 2020-01-04 10:55:36 +05:30
commit 729cada2fd
305 changed files with 6773 additions and 10169 deletions

2
.codacy.yml Normal file
View file

@ -0,0 +1,2 @@
exclude_paths:
- '**.sql'

2
.pylintrc Normal file
View file

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

View file

@ -1,6 +1,5 @@
language: python
dist: trusty
sudo: required
addons:
hosts:
@ -39,6 +38,16 @@ matrix:
env: DB=mariadb TYPE=server
script: bench --site test_site run-tests --coverage
before_install:
# install wkhtmltopdf
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
- tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
- sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
- sudo chmod o+x /usr/local/bin/wkhtmltopdf
# install cups
- sudo apt-get install libcups2-dev
install:
- cd ~
- source ./.nvm/nvm.sh
@ -52,23 +61,20 @@ install:
- mkdir ~/frappe-bench/sites/test_site
- cp $TRAVIS_BUILD_DIR/.travis/$DB.json ~/frappe-bench/sites/test_site/site_config.json
- mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
- mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
- if [ $DB == "mariadb" ];then
mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
mysql -u root -e "CREATE DATABASE test_frappe";
mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'";
mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'";
mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'";
mysql -u root -e "FLUSH PRIVILEGES";
fi
- mysql -u root -e "CREATE DATABASE test_frappe"
- mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
- mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
- mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
- mysql -u root -e "FLUSH PRIVILEGES"
- psql -c "CREATE DATABASE test_frappe" -U postgres
- psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
- tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
- sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
- sudo chmod o+x /usr/local/bin/wkhtmltopdf
- if [ $DB == "postgres" ];then
psql -c "CREATE DATABASE test_frappe" -U postgres;
psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
fi
- cd ./frappe-bench

View file

@ -0,0 +1,53 @@
export default {
name: 'Custom Submittable DocType',
custom: 1,
actions: [],
is_submittable: 1,
creation: '2019-12-10 06:29:07.215072',
doctype: 'DocType',
editable_grid: 1,
engine: 'InnoDB',
fields: [
{
fieldname: 'enabled',
fieldtype: 'Check',
label: 'Enabled',
allow_on_submit: 1,
reqd: 1
},
{
fieldname: 'title',
fieldtype: 'Data',
label: 'title',
reqd: 1
},
{
fieldname: 'description',
fieldtype: 'Text Editor',
label: 'Description'
}
],
links: [],
modified: '2019-12-10 14:40:53.127615',
modified_by: 'Administrator',
module: 'Custom',
owner: 'Administrator',
permissions: [
{
create: 1,
delete: 1,
email: 1,
print: 1,
read: 1,
role: 'System Manager',
share: 1,
write: 1,
submit: 1,
cancel: 1
}
],
quick_entry: 1,
sort_field: 'modified',
sort_order: 'ASC',
track_changes: 1
};

View file

@ -0,0 +1,48 @@
export default {
name: 'DateTime Test',
custom: 1,
actions: [],
creation: '2019-03-15 06:29:07.215072',
doctype: 'DocType',
editable_grid: 1,
engine: 'InnoDB',
fields: [
{
fieldname: 'date',
fieldtype: 'Date',
label: 'Date'
},
{
fieldname: 'time',
fieldtype: 'Time',
label: 'Time'
},
{
fieldname: 'datetime',
fieldtype: 'Datetime',
label: 'Datetime'
}
],
issingle: 1,
links: [],
modified: '2019-12-09 14:40:53.127615',
modified_by: 'Administrator',
module: 'Custom',
owner: 'Administrator',
permissions: [
{
create: 1,
delete: 1,
email: 1,
print: 1,
read: 1,
role: 'System Manager',
share: 1,
write: 1
}
],
quick_entry: 1,
sort_field: 'modified',
sort_order: 'ASC',
track_changes: 1
};

View file

@ -6,8 +6,8 @@ context('API Resources', () => {
});
it('Creates two Comments', () => {
cy.create_doc('Comment', {comment_type: 'Comment', content: "hello"});
cy.create_doc('Comment', {comment_type: 'Comment', content: "world"});
cy.insert_doc('Comment', {comment_type: 'Comment', content: "hello"});
cy.insert_doc('Comment', {comment_type: 'Comment', content: "world"});
});
it('Lists the Comments', () => {
@ -25,11 +25,11 @@ context('API Resources', () => {
});
it('Gets each Comment', () => {
cy.get_list('Comment').then(body => body.data.forEach(comment => {
cy.get_list('Comment').then(body => body.data.forEach(comment => {
cy.get_doc('Comment', comment.name);
}));
});
it('Removes the Comments', () => {
cy.get_list('Comment').then(body => body.data.forEach(comment => {
cy.remove_doc('Comment', comment.name);

View file

@ -0,0 +1,55 @@
context('Control Barcode', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
});
function get_dialog_with_barcode() {
return cy.dialog({
title: 'Barcode',
fields: [
{
label: 'Barcode',
fieldname: 'barcode',
fieldtype: 'Barcode'
}
]
});
}
it('should generate barcode on setting a value', () => {
get_dialog_with_barcode().as('dialog');
cy.get('.frappe-control[data-fieldname=barcode] input')
.focus()
.type('123456789')
.blur();
cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]')
.should('exist');
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('barcode');
expect(value).to.contain('<svg');
expect(value).to.contain('data-barcode-value="123456789"');
});
});
it('should reset when input is cleared', () => {
get_dialog_with_barcode().as('dialog');
cy.get('.frappe-control[data-fieldname=barcode] input')
.focus()
.type('123456789')
.blur();
cy.get('.frappe-control[data-fieldname=barcode] input')
.clear()
.blur();
cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]')
.should('not.exist');
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('barcode');
expect(value).to.equal('');
});
});
});

View file

@ -0,0 +1,128 @@
import datetime_doctype from '../fixtures/datetime_doctype';
const doctype_name = datetime_doctype.name;
context('Control Date, Time and DateTime', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.insert_doc('DocType', datetime_doctype, true);
});
describe('Date formats', () => {
let date_formats = [
{
date_format: 'dd-mm-yyyy',
part: 2,
length: 4,
separator: '-'
},
{
date_format: 'mm/dd/yyyy',
part: 0,
length: 2,
separator: '/'
}
];
date_formats.forEach(d => {
it('test date format ' + d.date_format, () => {
cy.set_value('System Settings', 'System Settings', {
date_format: d.date_format
});
cy.window()
.its('frappe')
.then(frappe => {
// update sys_defaults value to avoid a reload
frappe.sys_defaults.date_format = d.date_format;
});
cy.new_form(doctype_name);
cy.get('.form-control[data-fieldname=date]').focus();
cy.get('.datepickers-container .datepicker.active')
.should('be.visible');
cy.get(
'.datepickers-container .datepicker.active .datepicker--cell-day.-current-'
).click();
cy.window()
.its('cur_frm')
.then(cur_frm => {
let formatted_value = cur_frm.get_field('date').input.value;
let parts = formatted_value.split(d.separator);
expect(parts[d.part].length).to.equal(d.length);
});
});
});
});
describe('Time formats', () => {
let time_formats = [
{
time_format: 'HH:mm:ss',
value: ' 11:00:12',
match_value: '11:00:12'
},
{
time_format: 'HH:mm',
value: ' 11:00:12',
match_value: '11:00'
}
];
time_formats.forEach(d => {
it('test time format ' + d.time_format, () => {
cy.set_value('System Settings', 'System Settings', {
time_format: d.time_format
});
cy.window()
.its('frappe')
.then(frappe => {
frappe.sys_defaults.time_format = d.time_format;
});
cy.new_form(doctype_name);
cy.fill_field('time', d.value, 'Time').blur();
cy.get_field('time').should('have.value', d.match_value);
});
});
});
describe('DateTime formats', () => {
let datetime_formats = [
{
date_format: 'dd.mm.yyyy',
time_format: 'HH:mm:ss',
value: ' 02.12.2019 11:00:12',
doc_value: '2019-12-02 11:00:12',
input_value: '02.12.2019 11:00:12'
},
{
date_format: 'mm-dd-yyyy',
time_format: 'HH:mm',
value: ' 12-02-2019 11:00:00',
doc_value: '2019-12-02 11:00:00',
input_value: '12-02-2019 11:00'
}
];
datetime_formats.forEach(d => {
it(`test datetime format ${d.date_format} ${d.time_format}`, () => {
cy.set_value('System Settings', 'System Settings', {
date_format: d.date_format,
time_format: d.time_format
});
cy.window()
.its('frappe')
.then(frappe => {
frappe.sys_defaults.date_format = d.date_format;
frappe.sys_defaults.time_format = d.time_format;
});
cy.new_form(doctype_name);
cy.fill_field('datetime', d.value, 'Datetime').blur();
cy.get_field('datetime').should('have.value', d.input_value);
cy.window()
.its('cur_frm.doc.datetime')
.should('eq', d.doc_value);
});
});
});
});

View file

@ -0,0 +1,63 @@
context('Depends On', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
});
before(() => {
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
frappe.call('frappe.tests.ui_test_helpers.create_doctype', {
name: 'Test Depends On',
fields: [
{
"label": "Test Field",
"fieldname": "test_field",
"fieldtype": "Data",
},
{
"label": "Dependant Field",
"fieldname": "dependant_field",
"fieldtype": "Data",
"mandatory_depends_on": "eval:doc.test_field=='Some Value'",
"read_only_depends_on": "eval:doc.test_field=='Some Other Value'",
},
{
"label": "Display Dependant Field",
"fieldname": "display_dependant_field",
"fieldtype": "Data",
'depends_on': "eval:doc.test_field=='Value'"
},
]
});
});
});
it('should set the field as mandatory depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.fill_field('test_field', 'Some Value');
cy.get('button.primary-action').contains('Save').click();
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('be.visible');
cy.get('body').click();
cy.fill_field('test_field', 'Random value');
cy.get('button.primary-action').contains('Save').click();
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('not.be.visible');
});
it('should set the field as read only depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.fill_field('dependant_field', 'Some Value');
cy.fill_field('test_field', 'Some Other Value');
cy.get('body').click();
cy.get('.control-input [data-fieldname="dependant_field"]').should('be.disabled');
cy.fill_field('test_field', 'Random Value');
cy.get('body').click();
cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled');
});
it('should display the field depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible');
cy.get('.control-input [data-fieldname="test_field"]').clear();
cy.fill_field('test_field', 'Value');
cy.get('body').click();
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('be.visible');
});
});

View file

@ -2,8 +2,13 @@ context('Form', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
});
});
beforeEach(() => {
cy.visit('/desk');
});
it('create a new form', () => {
cy.visit('/desk#Form/ToDo/New ToDo 1');
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
@ -11,6 +16,29 @@ context('Form', () => {
cy.get('.primary-action').click();
cy.visit('/desk#List/ToDo');
cy.location('hash').should('eq', '#List/ToDo/List');
cy.get('h1').should('be.visible').and('contain', 'To Do');
cy.get('.list-row').should('contain', 'this is a test todo');
});
it('navigates between documents with child table list filters applied', () => {
cy.visit('/desk#List/Contact');
cy.location('hash').should('eq', '#List/Contact/List');
cy.get('.tag-filters-area .btn:contains("Add Filter")').click();
cy.get('.fieldname-select-area').should('exist');
cy.get('.fieldname-select-area input').type('Number{enter}', { force: true });
cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true });
cy.get('.filter-box .btn:contains("Apply")').click({ force: true });
cy.visit('/desk#Form/Contact/Test Form Contact 3');
cy.get('.prev-doc').should('be.visible').click();
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
cy.get('.btn-modal-close:visible').click();
cy.get('.next-doc').click();
cy.wait(200);
cy.contains('Test Form Contact 2').should('not.exist');
cy.get('.page-title .title-text').should('contain', 'Test Form Contact 1');
// clear filters
cy.window().its('frappe').then((frappe) => {
let list_view = frappe.get_list_view('Contact');
list_view.filter_area.filter_list.clear_filters();
});
});
});

View file

@ -0,0 +1,51 @@
context('Grid Pagination', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk');
});
before(() => {
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records");
});
});
it('creates pages for child table', () => {
cy.visit('/desk#Form/Contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').find('.current-page-number').should('contain', '1');
cy.get('@table').find('.total-page-number').should('contain', '20');
cy.get('@table').find('.grid-body .grid-row').should('have.length', 50);
});
it('goes to the next and previous page', () => {
cy.visit('/desk#Form/Contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').find('.next-page').click();
cy.get('@table').find('.current-page-number').should('contain', '2');
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '51');
cy.get('@table').find('.prev-page').click();
cy.get('@table').find('.current-page-number').should('contain', '1');
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '1');
});
it('adds and deletes rows and changes page', ()=> {
cy.visit('/desk#Form/Contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').find('button.grid-add-row').click();
cy.get('@table').find('.grid-body .row-index').should('contain', 1001);
cy.get('@table').find('.current-page-number').should('contain', '21');
cy.get('@table').find('.total-page-number').should('contain', '21');
cy.get('@table').find('.grid-body .grid-row .grid-row-check').click({force: true});
cy.get('@table').find('button.grid-remove-rows').click();
cy.get('@table').find('.grid-body .row-index').last().should('contain', 1000);
cy.get('@table').find('.current-page-number').should('contain', '20');
cy.get('@table').find('.total-page-number').should('contain', '20');
});
it('deletes all rows', ()=> {
cy.visit('/desk#Form/Contact/Test Contact');
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true});
cy.get('@table').find('button.grid-remove-all-rows').click();
cy.get('.modal-dialog .btn-primary').contains('Yes').click();
cy.get('@table').find('.grid-body .grid-row').should('have.length', 0);
});
});

View file

@ -2,10 +2,9 @@ context('List View', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.window().its('frappe').then(frappe => {
frappe.call("frappe.tests.ui_test_helpers.setup_workflow");
return cy.window().its('frappe').then(frappe => {
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow");
});
cy.clear_cache();
});
it('enables "Actions" button', () => {
const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Print', 'Delete'];

View file

@ -0,0 +1,40 @@
import custom_submittable_doctype from '../fixtures/custom_submittable_doctype';
const doctype_name = custom_submittable_doctype.name;
context('Report View', () => {
before(() => {
cy.login();
cy.visit('/desk');
cy.insert_doc('DocType', custom_submittable_doctype, true);
cy.clear_cache();
cy.insert_doc(doctype_name, {
'title': 'Doc 1',
'description': 'Random Text',
'enabled': 0,
// submit document
'docstatus': 1
}, true).as('doc');
});
it('Field with enabled allow_on_submit should be editable.', () => {
cy.server();
cy.route('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.visit(`/desk#List/${doctype_name}/Report`);
let cell = cy.get('.dt-row-0 > .dt-cell--col-3');
// select the cell
cell.dblclick();
cell.find('input[data-fieldname="enabled"]').check({force: true});
cy.get('.dt-row-0 > .dt-cell--col-4').click();
cy.wait('@value-update');
cy.get('@doc').then(doc => {
cy.call('frappe.client.get_value', {
doctype: doc.doctype,
filters: {
name: doc.name,
},
fieldname: 'enabled'
}).then(r => {
expect(r.message.enabled).to.equals(1);
});
});
});
});

View file

@ -42,95 +42,156 @@ Cypress.Commands.add('login', (email, password) => {
});
Cypress.Commands.add('call', (method, args) => {
return cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
url: `/api/method/${method}`,
method: 'POST',
body: args,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
}).then(res => {
expect(res.status).eq(200);
return res.body;
return cy
.window()
.its('frappe.csrf_token')
.then(csrf_token => {
return cy
.request({
url: `/api/method/${method}`,
method: 'POST',
body: args,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
})
.then(res => {
expect(res.status).eq(200);
return res.body;
});
});
});
});
Cypress.Commands.add('get_list', (doctype, fields=[], filters=[]) => {
return cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
method: 'GET',
url: `/api/resource/${doctype}?fields=${JSON.stringify(fields)}&filters=${JSON.stringify(filters)}`,
headers: {
'Accept': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
}).then(res => {
expect(res.status).eq(200);
return res.body;
Cypress.Commands.add('get_list', (doctype, fields = [], filters = []) => {
filters = JSON.stringify(filters);
fields = JSON.stringify(fields);
let url = `/api/resource/${doctype}?fields=${fields}&filters=${filters}`;
return cy
.window()
.its('frappe.csrf_token')
.then(csrf_token => {
return cy
.request({
method: 'GET',
url,
headers: {
Accept: 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
})
.then(res => {
expect(res.status).eq(200);
return res.body;
});
});
});
});
Cypress.Commands.add('get_doc', (doctype, name) => {
return cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
method: 'GET',
url: `/api/resource/${doctype}/${name}`,
headers: {
'Accept': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
}).then(res => {
expect(res.status).eq(200);
return res.body;
return cy
.window()
.its('frappe.csrf_token')
.then(csrf_token => {
return cy
.request({
method: 'GET',
url: `/api/resource/${doctype}/${name}`,
headers: {
Accept: 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
})
.then(res => {
expect(res.status).eq(200);
return res.body;
});
});
});
});
Cypress.Commands.add('create_doc', (doctype, args) => {
return cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
method: 'POST',
url: `/api/resource/${doctype}`,
body: args,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
}).then(res => {
expect(res.status).eq(200);
return res.body;
Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
return cy
.window()
.its('frappe.csrf_token')
.then(csrf_token => {
return cy
.request({
method: 'POST',
url: `/api/resource/${doctype}`,
body: args,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
},
failOnStatusCode: !ignore_duplicate
})
.then(res => {
let status_codes = [200];
if (ignore_duplicate) {
status_codes.push(409);
}
expect(res.status).to.be.oneOf(status_codes);
return res.body;
});
});
});
});
Cypress.Commands.add('remove_doc', (doctype, name) => {
return cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
method: 'DELETE',
url: `/api/resource/${doctype}/${name}`,
headers: {
'Accept': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
}).then(res => {
expect(res.status).eq(202);
return res.body;
return cy
.window()
.its('frappe.csrf_token')
.then(csrf_token => {
return cy
.request({
method: 'DELETE',
url: `/api/resource/${doctype}/${name}`,
headers: {
Accept: 'application/json',
'X-Frappe-CSRF-Token': csrf_token
}
})
.then(res => {
expect(res.status).eq(202);
return res.body;
});
});
});
});
Cypress.Commands.add('create_records', (doc) => {
return cy.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
Cypress.Commands.add('create_records', doc => {
return cy
.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
.then(r => r.message);
});
Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => {
Cypress.Commands.add('set_value', (doctype, name, obj) => {
return cy.call('frappe.client.set_value', {
doctype,
name,
fieldname: obj
});
});
Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
cy.get_field(fieldname, fieldtype).as('input');
if (['Date', 'Time', 'Datetime'].includes(fieldtype)) {
cy.get('@input').click().wait(200);
cy.get('.datepickers-container .datepicker.active').should('exist');
}
if (fieldtype === 'Time') {
cy.get('@input').clear();
}
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
cy.get('@input').type(value, { waitForAnimations: false });
}
return cy.get('@input');
});
Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
let selector = `.form-control[data-fieldname="${fieldname}"]`;
if (fieldtype === 'Text Editor') {
@ -140,34 +201,33 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype='Data') => {
selector = `[data-fieldname="${fieldname}"] .ace_text-input`;
}
cy.get(selector).as('input');
if (fieldtype === 'Select') {
return cy.get('@input').select(value);
} else {
return cy.get('@input').type(value, {waitForAnimations: false});
}
return cy.get(selector);
});
Cypress.Commands.add('awesomebar', (text) => {
Cypress.Commands.add('awesomebar', text => {
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('new_form', doctype => {
let route = `Form/${doctype}/New ${doctype} 1`;
cy.visit(`/desk#${route}`);
cy.get('body').should('have.attr', 'data-route', route);
cy.get('body').should('have.attr', 'data-ajax-state', 'complete');
});
Cypress.Commands.add('go_to_list', (doctype) => {
Cypress.Commands.add('go_to_list', doctype => {
cy.visit(`/desk#List/${doctype}/List`);
});
Cypress.Commands.add('clear_cache', () => {
cy.window().its('frappe').then(frappe => {
frappe.ui.toolbar.clear_cache();
});
cy.window()
.its('frappe')
.then(frappe => {
frappe.ui.toolbar.clear_cache();
});
});
Cypress.Commands.add('dialog', (opts) => {
Cypress.Commands.add('dialog', opts => {
return cy.window().then(win => {
var d = new win.frappe.ui.Dialog(opts);
d.show();
@ -180,6 +240,36 @@ Cypress.Commands.add('get_open_dialog', () => {
});
Cypress.Commands.add('hide_dialog', () => {
cy.get_open_dialog().find('.btn-modal-close').click();
cy.get_open_dialog()
.find('.btn-modal-close')
.click();
cy.get('.modal:visible').should('not.exist');
});
Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
return cy
.window()
.its('frappe.csrf_token')
.then(csrf_token => {
return cy
.request({
method: 'POST',
url: `/api/resource/${doctype}`,
body: args,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
},
failOnStatusCode: !ignore_duplicate
})
.then(res => {
let status_codes = [200];
if (ignore_duplicate) {
status_codes.push(409);
}
expect(res.status).to.be.oneOf(status_codes);
return res.body.data;
});
});
});

View file

@ -23,7 +23,7 @@ if sys.version[0] == '2':
reload(sys)
sys.setdefaultencoding("utf-8")
__version__ = '12.0.17'
__version__ = '12.1.0'
__title__ = "Frappe Framework"
local = Local()
@ -123,7 +123,6 @@ def init(site, sites_path=None, new_site=False):
local.debug_log = []
local.realtime_log = []
local.flags = _dict({
"ran_schedulers": [],
"currently_saving": [],
"redirect_location": "",
"in_install_db": False,
@ -290,7 +289,7 @@ def log(msg):
debug_log.append(as_unicode(msg))
def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False):
def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None, alert=False, primary_action=None):
"""Print a message to the user (via HTTP response).
Messages are sent in the `__server_messages` property in the
response JSON and shown in a pop-up / modal.
@ -299,6 +298,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
:param title: [optional] Message title.
:param raise_exception: [optional] Raise given exception and show message.
:param as_table: [optional] If `msg` is a list of lists, render as HTML table.
:param primary_action: [optional] Bind a primary server/client side action.
"""
from frappe.utils import encode
@ -338,6 +338,9 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
if alert:
out.alert = 1
if primary_action:
out.primary_action = primary_action
message_log.append(json.dumps(out))
if raise_exception and hasattr(raise_exception, '__name__'):
@ -1504,7 +1507,22 @@ def logger(module=None, with_more_info=True):
def log_error(message=None, title=None):
'''Log error to Error Log'''
return get_doc(dict(doctype='Error Log', error=as_unicode(message or get_traceback()),
# AI ALERT:
# the title and message may be swapped
# the better API for this is log_error(title, message), and used in many cases this way
# this hack tries to be smart about whats a title (single line ;-)) and fixes it
if message:
if '\n' not in message:
title = message
error = get_traceback()
else:
error = message
else:
error = get_traceback()
return get_doc(dict(doctype='Error Log', error=as_unicode(error),
method=title)).insert(ignore_permissions=True)
def get_desk_link(doctype, name):

View file

@ -82,7 +82,7 @@ def handle():
if frappe.local.request.method=="PUT":
if frappe.local.form_dict.data is None:
data = json.loads(frappe.local.request.get_data())
data = json.loads(frappe.safe_decode(frappe.local.request.get_data()))
else:
data = json.loads(frappe.local.form_dict.data)
doc = frappe.get_doc(doctype, name)
@ -117,7 +117,7 @@ def handle():
if frappe.local.request.method=="POST":
if frappe.local.form_dict.data is None:
data = json.loads(frappe.local.request.get_data())
data = json.loads(frappe.safe_decode(frappe.local.request.get_data()))
else:
data = json.loads(frappe.local.form_dict.data)
data.update({

View file

@ -142,6 +142,7 @@ class LoginManager:
self.validate_hour()
self.get_user_info()
self.make_session()
self.setup_boot_cache()
self.set_user_info()
def get_user_info(self, resume=False):
@ -150,6 +151,11 @@ class LoginManager:
self.user_type = self.info.user_type
def setup_boot_cache(self):
frappe.cache_manager.build_table_count_cache()
frappe.cache_manager.build_domain_restriced_doctype_cache()
frappe.cache_manager.build_domain_restriced_page_cache()
def set_user_info(self, resume=False):
# set sid again
frappe.local.cookie_manager.init_cookies()

View file

@ -31,12 +31,6 @@ class AssignmentRule(Document):
return False
def apply_close(self, doc, assignments):
if (self.close_assignments and
self.name in [d.assignment_rule for d in assignments]):
return self.close_assignments(doc)
return False
def apply_assign(self, doc):
if self.safe_eval('assign_condition', doc):
@ -157,16 +151,17 @@ def bulk_apply(doctype, docnames):
apply(None, doctype=doctype, name=name)
def reopen_closed_assignment(doc):
todo = frappe.db.exists('ToDo', dict(
todo_list = frappe.db.get_all('ToDo', filters = dict(
reference_type = doc.doctype,
reference_name = doc.name,
status = 'Closed'
))
if not todo:
if not todo_list:
return False
todo = frappe.get_doc("ToDo", todo)
todo.status = 'Open'
todo.save(ignore_permissions=True)
for todo in todo_list:
todo_doc = frappe.get_doc('ToDo', todo.name)
todo_doc.status = 'Open'
todo_doc.save(ignore_permissions=True)
return True
def apply(doc, method=None, doctype=None, name=None):
@ -225,13 +220,12 @@ def apply(doc, method=None, doctype=None, name=None):
continue
if not new_apply:
reopen = reopen_closed_assignment(doc)
if reopen:
break
close = assignment_rule.apply_close(doc, assignments)
if close:
break
# only reopen if close condition is not satisfied
if not assignment_rule.safe_eval('close_condition', doc):
reopen = reopen_closed_assignment(doc)
if reopen:
break
assignment_rule.close_assignments(doc)
def get_assignment_rules():
return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))]

View file

@ -9,7 +9,7 @@ from frappe.desk.form import assign_to
from frappe.utils.jinja import validate_template
from dateutil.relativedelta import relativedelta
from frappe.utils.user import get_system_managers
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day, month_diff
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils.background_jobs import get_jobs
@ -48,7 +48,7 @@ class AutoRepeat(Document):
if self.disabled:
self.next_schedule_date = None
else:
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
def unlink_if_applicable(self):
if self.status == 'Completed' or self.disabled:
@ -105,32 +105,29 @@ class AutoRepeat(Document):
schedule_details = []
start_date = getdate(self.start_date)
end_date = getdate(self.end_date)
today = frappe.utils.datetime.date.today()
if start_date < today:
start_date = today
if not self.end_date:
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day)
row = {
"reference_document": self.reference_document,
"frequency": self.frequency,
"next_scheduled_date": start_date
"next_scheduled_date": next_date
}
schedule_details.append(row)
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
if self.end_date:
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
while (getdate(start_date) < getdate(end_date)):
next_date = get_next_schedule_date(
start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True)
while (getdate(next_date) < getdate(end_date)):
row = {
"reference_document" : self.reference_document,
"frequency" : self.frequency,
"next_scheduled_date" : start_date
"next_scheduled_date" : next_date
}
schedule_details.append(row)
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date)
next_date = get_next_schedule_date(
next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True)
return schedule_details
@ -271,18 +268,34 @@ class AutoRepeat(Document):
)
def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day = False, end_date = None):
month_count = month_map.get(frequency)
def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False):
if month_map.get(frequency):
month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1
else:
month_count = 0
day_count = 0
if month_count and repeat_on_last_day:
next_date = get_next_date(start_date, month_count, 31)
day_count = 31
next_date = get_next_date(start_date, month_count, day_count)
elif month_count and repeat_on_day:
next_date = get_next_date(start_date, month_count, repeat_on_day)
day_count = repeat_on_day
next_date = get_next_date(start_date, month_count, day_count)
elif month_count:
next_date = get_next_date(start_date, month_count)
else:
days = 7 if frequency == 'Weekly' else 1
next_date = add_days(start_date, days)
# next schedule date should be after or on current date
if not for_full_schedule:
while getdate(next_date) < getdate(today()):
if month_count:
month_count += month_map.get(frequency)
next_date = get_next_date(start_date, month_count, day_count)
return next_date
def get_next_date(dt, mcount, day=None):
@ -307,10 +320,9 @@ def create_repeated_entries(data):
current_date = getdate(today())
schedule_date = getdate(doc.next_schedule_date)
while schedule_date <= current_date and not doc.disabled:
if schedule_date == current_date and not doc.disabled:
doc.create_documents()
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)
if schedule_date and not doc.disabled:
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)

View file

@ -96,6 +96,21 @@ class TestAutoRepeat(unittest.TestCase):
linked_comm = frappe.db.exists("Communication", dict(reference_doctype="ToDo", reference_name=new_todo))
self.assertTrue(linked_comm)
def test_next_schedule_date(self):
current_date = getdate(today())
todo = frappe.get_doc(
dict(doctype='ToDo', description='test next schedule date todo', assigned_by='Administrator')).insert()
doc = make_auto_repeat(frequency='Monthly', reference_document=todo.name, start_date=add_months(today(), -2))
#check next_schedule_date is set as per current date
#it should not be a previous month's date
self.assertEqual(doc.next_schedule_date, current_date)
data = get_auto_repeat_entries(current_date)
create_repeated_entries(data)
docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
#the original doc + the repeated doc
self.assertEqual(len(docnames), 2)
def make_auto_repeat(**args):
args = frappe._dict(args)

View file

@ -2,7 +2,7 @@
# MIT License. See license.txt
from __future__ import unicode_literals, print_function
import os, frappe, json, shutil, re, warnings
import os, frappe, json, shutil, re, warnings, tempfile
from os.path import exists as path_exists, join as join_path, abspath, isdir
from distutils.spawn import find_executable
from six import iteritems, text_type
@ -12,6 +12,51 @@ from frappe.utils.minify import JavascriptMinify
Build the `public` folders and setup languages
"""
def symlink(target, link_name, overwrite=False):
'''
Create a symbolic link named link_name pointing to target.
If link_name exists then FileExistsError is raised, unless overwrite=True.
When trying to overwrite a directory, IsADirectoryError is raised.
Source: https://stackoverflow.com/a/55742015/10309266
'''
if not overwrite:
os.symlink(target, linkname)
return
# os.replace() may fail if files are on different filesystems
link_dir = os.path.dirname(link_name)
# Create link to target with temporary filename
while True:
temp_link_name = tempfile.mktemp(dir=link_dir)
# os.* functions mimic as closely as possible system functions
# The POSIX symlink() returns EEXIST if link_name already exists
# https://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html
try:
os.symlink(target, temp_link_name)
break
except FileExistsError:
pass
# Replace link_name with temp_link_name
try:
# Pre-empt os.replace on a directory with a nicer message
if os.path.isdir(link_name):
raise IsADirectoryError("Cannot symlink over existing directory: '{}'".format(link_name))
try:
os.replace(temp_link_name, link_name)
except AttributeError:
os.renames(temp_link_name, link_name)
except:
if os.path.islink(temp_link_name):
os.remove(temp_link_name)
raise
app_paths = None
def setup():
global app_paths
@ -118,7 +163,7 @@ def make_asset_dirs(make_copy=False, restore=False):
else:
shutil.rmtree(target)
try:
os.symlink(source, target)
symlink(source, target, overwrite=True)
except OSError:
print('Cannot link {} to {}'.format(source, target))
else:

View file

@ -115,3 +115,46 @@ def get_doctype_map(doctype, name, filters, order_by=None):
def clear_doctype_map(doctype, name):
cache_key = frappe.scrub(doctype) + '_map'
frappe.cache().hdel(cache_key, name)
def build_table_count_cache():
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
return
_cache = frappe.cache()
data = frappe.db.multisql({
"mariadb": """
SELECT table_name AS name,
table_rows AS count
FROM information_schema.tables""",
"postgres": """
SELECT "relname" AS name,
"n_tup_ins" AS count
FROM "pg_stat_all_tables"
"""
}, as_dict=1)
counts = {d.get('name').lstrip('tab'): d.get('count', None) for d in data}
_cache.set_value("information_schema:counts", counts)
return counts
def build_domain_restriced_doctype_cache():
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
return
_cache = frappe.cache()
active_domains = frappe.get_active_domains()
doctypes = frappe.get_all("DocType", filters={'restrict_to_domain': ('IN', active_domains)})
doctypes = [doc.name for doc in doctypes]
_cache.set_value("domain_restricted_doctypes", doctypes)
return doctypes
def build_domain_restriced_page_cache():
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
return
_cache = frappe.cache()
active_domains = frappe.get_active_domains()
pages = frappe.get_all("Page", filters={'restrict_to_domain': ('IN', active_domains)})
pages = [page.name for page in pages]
_cache.set_value("domain_restricted_pages", pages)
return pages

View file

@ -22,8 +22,7 @@
"fieldtype": "Link",
"label": "User",
"options": "User",
"reqd": 1,
"unique": 1
"reqd": 1
},
{
"default": "Online",

View file

@ -1,17 +1,14 @@
from __future__ import unicode_literals
# imports - standard imports
import json
# imports - module imports
from frappe.model.document import Document
from frappe import _
from frappe.model.document import Document
from frappe import _
import frappe
# imports - frappe module imports
from frappe.chat import authenticate
from frappe.chat import authenticate
from frappe.core.doctype.version.version import get_diff
from frappe.chat.doctype.chat_message import chat_message
from frappe.chat.doctype.chat_message import chat_message
from frappe.chat.util import (
safe_json_loads,
dictify,
@ -22,13 +19,14 @@ from frappe.chat.util import (
session = frappe.session
def is_direct(owner, other, bidirectional = False):
def is_direct(owner, other, bidirectional=False):
def get_room(owner, other):
room = frappe.get_all('Chat Room', filters = [
['Chat Room', 'type' , 'in', ('Direct', 'Visitor')],
['Chat Room', 'owner', '=' , owner],
['Chat Room User', 'user' , '=' , other]
], distinct = True)
room = frappe.get_all('Chat Room', filters=[
['Chat Room', 'type', 'in', ('Direct', 'Visitor')],
['Chat Room', 'owner', '=', owner],
['Chat Room User', 'user', '=', other]
], distinct=True)
return room
@ -38,7 +36,8 @@ def is_direct(owner, other, bidirectional = False):
return exists
def get_chat_room_user_set(users, filter_ = None):
def get_chat_room_user_set(users, filter_=None):
seen, uset = set(), list()
for u in users:
@ -48,12 +47,13 @@ def get_chat_room_user_set(users, filter_ = None):
return uset
class ChatRoom(Document):
def validate(self):
if self.is_new():
users = get_chat_room_user_set(self.users, filter_ = lambda u: u.user != session.user)
users = get_chat_room_user_set(self.users, filter_=lambda u: u.user != session.user)
self.update(dict(
users = users
users=users
))
if self.type == "Direct":
@ -63,7 +63,7 @@ class ChatRoom(Document):
other = squashify(self.users)
if self.is_new():
if is_direct(self.owner, other.user, bidirectional = True):
if is_direct(self.owner, other.user, bidirectional=True):
frappe.throw(_('Direct room with {0} already exists.').format(other.user))
if self.type == "Group" and not self.room_name:
@ -74,40 +74,44 @@ class ChatRoom(Document):
before = self.get_doc_before_save()
if not before: return
after = self
diff = dictify(get_diff(before, after))
after = self
diff = dictify(get_diff(before, after))
if diff:
update = { }
update = {}
for changed in diff.changed:
field, old, new = changed
if field == 'last_message':
new = chat_message.get(new)
update.update({ field: new })
update.update({field: new})
if diff.added or diff.removed:
update.update(dict(users = [u.user for u in self.users]))
update.update(dict(users=[u.user for u in self.users]))
update = dict(room = self.name, data = update)
update = dict(room=self.name, data=update)
frappe.publish_realtime('frappe.chat.room:update', update, room = self.name, after_commit = True)
frappe.publish_realtime('frappe.chat.room:update', update, room=self.name,
after_commit=True)
@frappe.whitelist(allow_guest = True)
def get(user, rooms = None, fields = None, filters = None):
@frappe.whitelist(allow_guest=True)
def get(user=None, token=None, rooms=None, fields=None, filters=None):
# There is this horrible bug out here.
# Looks like if frappe.call sends optional arguments (not in right order), the argument turns to an empty string.
# Looks like if frappe.call sends optional arguments (not in right order),
# the argument turns to an empty string.
# I'm not even going to think searching for it.
# Hence, the hack was get_if_empty (previous assign_if_none)
# - Achilles Rasquinha achilles@frappe.io
authenticate(user)
data = user or token
authenticate(data)
rooms, fields, filters = safe_json_loads(rooms, fields, filters)
rooms = listify(get_if_empty(rooms, [ ]))
fields = listify(get_if_empty(fields, [ ]))
rooms = listify(get_if_empty(rooms, []))
fields = listify(get_if_empty(fields, []))
const = [ ] # constraints
const = [] # constraints
if rooms:
const.append(['Chat Room', 'name', 'in', rooms])
if filters:
@ -117,24 +121,24 @@ def get(user, rooms = None, fields = None, filters = None):
const.append(filters)
default = ['name', 'type', 'room_name', 'creation', 'owner', 'avatar']
handle = ['users', 'last_message']
handle = ['users', 'last_message']
param = [f for f in fields if f not in handle]
param = [f for f in fields if f not in handle]
rooms = frappe.get_all('Chat Room',
or_filters = [
['Chat Room', 'owner', '=', user],
['Chat Room User', 'user', '=', user]
],
filters = const,
fields = param + ['name'] if param else default,
distinct = True
)
rooms = frappe.get_all('Chat Room',
or_filters=[
['Chat Room', 'owner', '=', frappe.session.user],
['Chat Room User', 'user', '=', frappe.session.user]
],
filters=const,
fields=param + ['name'] if param else default,
distinct=True
)
if not fields or 'users' in fields:
for i, r in enumerate(rooms):
droom = frappe.get_doc('Chat Room', r.name)
rooms[i]['users'] = [ ]
rooms[i]['users'] = []
for duser in droom.users:
rooms[i]['users'].append(duser.user)
@ -151,46 +155,47 @@ def get(user, rooms = None, fields = None, filters = None):
return rooms
@frappe.whitelist(allow_guest = True)
def create(kind, owner, users = None, name = None):
authenticate(owner)
users = safe_json_loads(users)
@frappe.whitelist(allow_guest=True)
def create(kind, token, users=None, name=None):
authenticate(token)
users = safe_json_loads(users)
create = True
if kind == 'Visitor':
room = squashify(frappe.db.sql("""
SELECT name
FROM `tabChat Room`
WHERE owner = "{owner}"
""".format(owner = owner), as_dict = True))
WHERE owner=%s
""", (frappe.session.user), as_dict=True))
if room:
room = frappe.get_doc('Chat Room', room.name)
room = frappe.get_doc('Chat Room', room.name)
create = False
if create:
room = frappe.new_doc('Chat Room')
room.type = kind
room.owner = owner
room = frappe.new_doc('Chat Room')
room.type = kind
room.owner = frappe.session.user
room.room_name = name
dusers = [ ]
dusers = []
if kind != 'Visitor':
if users:
users = listify(users)
users = listify(users)
for user in users:
duser = frappe.new_doc('Chat Room User')
duser = frappe.new_doc('Chat Room User')
duser.user = user
dusers.append(duser)
room.users = dusers
else:
dsettings = frappe.get_single('Website Settings')
dsettings = frappe.get_single('Website Settings')
room.room_name = dsettings.chat_room_name
users = [user for user in room.users] if hasattr(room, 'users') else [ ]
users = [user for user in room.users] if hasattr(room, 'users') else []
for user in dsettings.chat_operators:
if user.user not in users:
@ -199,24 +204,26 @@ def create(kind, owner, users = None, name = None):
chat_room_user = {"doctype": "Chat Room User", "user": user.user}
room.append('users', chat_room_user)
room.save(ignore_permissions = True)
room.save(ignore_permissions=True)
room = get(owner, rooms = room.name)
users = [room.owner] + [u for u in room.users]
room = get(token=token, rooms=room.name)
if room:
users = [room.owner] + [u for u in room.users]
for u in users:
frappe.publish_realtime('frappe.chat.room:create', room, user = u, after_commit = True)
for user in users:
frappe.publish_realtime('frappe.chat.room:create', room, user=user, after_commit=True)
return room
@frappe.whitelist(allow_guest = True)
def history(room, user, fields = None, limit = 10, start = None, end = None):
@frappe.whitelist(allow_guest=True)
def history(room, user, fields=None, limit=10, start=None, end=None):
if frappe.get_doc('Chat Room', room).type != 'Visitor':
authenticate(user)
fields = safe_json_loads(fields)
mess = chat_message.history(room, limit = limit, start = start, end = end)
mess = squashify(mess)
mess = chat_message.history(room, limit=limit, start=start, end=end)
mess = squashify(mess)
return dictify(mess)
return dictify(mess)

View file

@ -16,8 +16,7 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Token",
"reqd": 1,
"unique": 1
"reqd": 1
},
{
"fieldname": "ip_address",

View file

@ -13,6 +13,8 @@ from six import text_type
@click.argument('site')
@click.option('--db-name', help='Database name')
@click.option('--db-type', default='mariadb', type=click.Choice(['mariadb', 'postgres']), help='Optional "postgres" or "mariadb". Default is "mariadb"')
@click.option('--db-host', help='Database Host')
@click.option('--db-port', type=int, help='Database Port')
@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB')
@click.option('--mariadb-root-password', help='Root password for MariaDB')
@click.option('--admin-password', help='Administrator password for new site', default=None)
@ -21,22 +23,22 @@ from six import text_type
@click.option('--source_sql', help='Initiate database with a SQL file')
@click.option('--install-app', multiple=True, help='Install app after installation')
def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None,
verbose=False, install_apps=None, source_sql=None, force=None, install_app=None,
db_name=None, db_type=None):
verbose=False, install_apps=None, source_sql=None, force=None, install_app=None,
db_name=None, db_type=None, db_host=None, db_port=None):
"Create a new site"
frappe.init(site=site, new_site=True)
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
db_type=db_type)
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
db_type=db_type, db_host=db_host, db_port=db_port)
if len(frappe.utils.get_sites()) == 1:
use(site)
def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None,
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
reinstall=False, db_type=None):
admin_password=None, verbose=False, install_apps=None, source_sql=None, force=False,
reinstall=False, db_type=None, db_host=None, db_port=None):
"""Install a new Frappe site"""
if not force and os.path.exists(site):
@ -65,8 +67,8 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
installing = touch_file(get_site_path('locks', 'installing.lock'))
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password,
db_name=db_name, admin_password=admin_password, verbose=verbose,
source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type)
db_name=db_name, admin_password=admin_password, verbose=verbose,
source_sql=source_sql, force=force, reinstall=reinstall, db_type=db_type, db_host=db_host, db_port=db_port)
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
for app in apps_to_install:

View file

@ -460,6 +460,15 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
tests = test
site = get_site(context)
allow_tests = frappe.get_conf(site).allow_tests
if not (allow_tests or os.environ.get('CI')):
click.secho('Testing is disabled for the site!', bold=True)
click.secho('You can enable tests by entering following command:')
click.secho('bench --site {0} set-config allow_tests true'.format(site), fg='green')
return
frappe.init(site=site)
frappe.flags.skip_before_tests = skip_before_tests
@ -507,26 +516,6 @@ def run_ui_tests(context, app, headless=False):
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
@click.command('run-setup-wizard-ui-test')
@click.option('--app', help="App to run tests on, leave blank for all apps")
@click.option('--profile', is_flag=True, default=False)
@pass_context
def run_setup_wizard_ui_test(context, app=None, profile=False):
"Run setup wizard UI test"
import frappe.test_runner
site = get_site(context)
frappe.init(site=site)
frappe.connect()
ret = frappe.test_runner.run_setup_wizard_ui_test(app=app, verbose=context.verbose,
profile=profile)
if len(ret.failures) == 0 and len(ret.errors) == 0:
ret = 0
if os.environ.get('CI'):
sys.exit(ret)
@click.command('serve')
@click.option('--port', default=8000)
@click.option('--profile', is_flag=True, default=False)
@ -752,7 +741,6 @@ commands = [
reset_perms,
run_tests,
run_ui_tests,
run_setup_wizard_ui_test,
serve,
set_config,
show_config,

View file

@ -1,4 +1,5 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.desk.moduleview import add_setup_section
@ -88,7 +89,7 @@ def get_data():
]
},
{
"label": _("Email"),
"label": _("Email / Notifications"),
"icon": "fa fa-envelope",
"items": [
{
@ -120,6 +121,12 @@ def get_data():
"type": "doctype",
"name": "Newsletter",
"description": _("Create and manage newsletter")
},
{
"type": "doctype",
"route": "Form/Notification Settings/{}".format(frappe.session.user),
"name": "Notification Settings",
"description": _("Configure notifications for mentions, assignments, energy points and more.")
}
]
},

View file

@ -60,10 +60,6 @@ class Address(Document):
if not [row for row in self.links if row.link_doctype == "Company"]:
frappe.throw(_("Company is mandatory, as it is your company address"))
# removing other links
to_remove = [row for row in self.links if row.link_doctype != "Company"]
[ self.remove(row) for row in to_remove ]
def get_display(self):
return get_address_display(self.as_dict())
@ -145,30 +141,10 @@ def get_list_context(context=None):
def get_address_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by = None):
from frappe.www.list import get_list
user = frappe.session.user
ignore_permissions = False
if is_website_user():
if not filters: filters = []
add_name = []
contact = frappe.db.sql("""
select
address.name
from
`tabDynamic Link` as link
join
`tabAddress` as address on link.parent = address.name
where
link.parenttype = 'Address' and
link_name in(
select
link.link_name from `tabContact` as contact
join
`tabDynamic Link` as link on contact.name = link.parent
where
contact.user = %s)""",(user))
for c in contact:
add_name.append(c[0])
filters.append(("Address", "name", "in", add_name))
ignore_permissions = True
ignore_permissions = True
if not filters: filters = []
filters.append(("Address", "owner", "=", user))
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions)

View file

@ -81,9 +81,9 @@ def get_feed_match_conditions(user=None, doctype='Comment'):
if user_permissions:
can_read_docs = []
for doctype, obj in user_permissions.items():
for dt, obj in user_permissions.items():
for n in obj:
can_read_docs.append('{}|{}'.format(doctype, frappe.db.escape(n.get('doc', ''))))
can_read_docs.append('{}|{}'.format(frappe.db.escape(dt), frappe.db.escape(n.get('doc', ''))))
if can_read_docs:
conditions.append("concat_ws('|', `tab{doctype}`.reference_doctype, `tab{doctype}`.reference_name) in ({values})".format(

View file

@ -18,6 +18,10 @@ frappe.ui.form.on("Communication", {
frm.convert_to_click && frm.set_convert_button();
frm.subject_field = "subject";
// content field contains weird table html that does not render well in Quill
// this field is not to be edited directly anyway, so setting it as read only
frm.set_df_property('content', 'read_only', 1);
if(frm.doc.reference_doctype && frm.doc.reference_name) {
frm.add_custom_button(__(frm.doc.reference_name), function() {
frappe.set_route("Form", frm.doc.reference_doctype, frm.doc.reference_name);

View file

@ -351,16 +351,26 @@ def get_contacts(email_strings):
email = get_email_without_link(email)
contact_name = get_contact_name(email)
if not contact_name:
contact = frappe.get_doc({
"doctype": "Contact",
"first_name": frappe.unscrub(email.split("@")[0]),
})
contact.add_email(email_id=email, is_primary=True)
contact.insert(ignore_permissions=True)
contact_name = contact.name
if not contact_name and email:
email_parts = email.split("@")
first_name = frappe.unscrub(email_parts[0])
contacts.append(contact_name)
try:
contact_name = '{0}-{1}'.format(first_name, email_parts[1]) if first_name == 'Contact' else first_name
contact = frappe.get_doc({
"doctype": "Contact",
"first_name": contact_name,
"name": contact_name
})
contact.add_email(email_id=email, is_primary=True)
contact.insert(ignore_permissions=True)
contact_name = contact.name
except Exception:
traceback = frappe.get_traceback()
frappe.log_error(traceback)
if contact_name:
contacts.append(contact_name)
return contacts

View file

@ -10,7 +10,6 @@ from email.utils import formataddr
from frappe.core.utils import get_parent_doc
from frappe.utils import (get_url, get_formatted_email, cint,
validate_email_address, split_emails, time_diff_in_seconds, parse_addr, get_datetime)
from frappe.utils.scheduler import log
from frappe.email.email_body import get_message_id
import frappe.email.smtp
import time
@ -239,8 +238,9 @@ def get_recipients_cc_and_bcc(doc, recipients, cc, bcc, fetched_from_email_accou
return recipients, cc, bcc
def remove_administrator_from_email_list(email_list):
if 'Administrator' in email_list:
email_list.remove('Administrator')
administrator_email = list(filter(lambda emails: "Administrator" in emails, email_list))
if administrator_email:
email_list.remove(administrator_email[0])
def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None):
"""Prepare to make multipart MIME Email
@ -305,27 +305,12 @@ def set_incoming_outgoing_accounts(doc):
doc.incoming_email_account = frappe.db.get_value("Email Account",
{"append_to": doc.reference_doctype, }, "email_id")
doc.outgoing_email_account = frappe.db.get_value("Email Account",
{"append_to": doc.reference_doctype, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender", "name",
"always_use_account_name_as_sender_name"], as_dict=True)
if not doc.incoming_email_account:
doc.incoming_email_account = frappe.db.get_value("Email Account",
{"default_incoming": 1, "enable_incoming": 1}, "email_id")
if not doc.outgoing_email_account:
# if from address is not the default email account
doc.outgoing_email_account = frappe.db.get_value("Email Account",
{"email_id": doc.sender, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender", "name",
"send_unsubscribe_message", "always_use_account_name_as_sender_name"], as_dict=True) or frappe._dict()
if not doc.outgoing_email_account:
doc.outgoing_email_account = frappe.db.get_value("Email Account",
{"default_outgoing": 1, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender", "name",
"send_unsubscribe_message", "always_use_account_name_as_sender_name"],as_dict=True) or frappe._dict()
doc.outgoing_email_account = frappe.email.smtp.get_outgoing_email_account(raise_exception_not_set=False,
append_to=doc.doctype, sender=doc.sender)
if doc.sent_or_received == "Sent":
doc.db_set("email_account", doc.outgoing_email_account.name)
@ -399,7 +384,7 @@ def get_bcc(doc, recipients=None, fetched_from_email_account=False):
return bcc
def add_attachments(name, attachments):
'''Add attachments to the given Communiction'''
'''Add attachments to the given Communication'''
# loop through attachments
for a in attachments:
if isinstance(a, string_types):
@ -412,7 +397,9 @@ def add_attachments(name, attachments):
"file_url": attach.file_url,
"attached_to_doctype": "Communication",
"attached_to_name": name,
"folder": "Home/Attachments"})
"folder": "Home/Attachments",
"is_private": attach.is_private
})
_file.save(ignore_permissions=True)
def filter_email_list(doc, email_list, exclude, is_cc=False, is_bcc=False):
@ -509,17 +496,7 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments
break
except:
traceback = log("frappe.core.doctype.communication.email.sendmail", frappe.as_json({
"communication_name": communication_name,
"print_html": print_html,
"print_format": print_format,
"attachments": attachments,
"recipients": recipients,
"cc": cc,
"bcc": bcc,
"lang": lang
}))
frappe.logger(__name__).error(traceback)
traceback = frappe.log_error("frappe.core.doctype.communication.email.sendmail")
raise
def update_mins_to_first_communication(parent, communication):
@ -552,4 +529,4 @@ def mark_email_as_seen(name=None):
frappe.response["type"] = 'binary'
frappe.response["filename"] = "imaginary_pixel.png"
frappe.response["filecontent"] = buffered_obj.getvalue()
frappe.response["filecontent"] = buffered_obj.getvalue()

View file

@ -3,19 +3,20 @@
frappe.ui.form.on('Data Import', {
onload: function(frm) {
if(frm.doc.__islocal) {
if (frm.doc.__islocal) {
frm.set_value("action", "");
}
frappe.call({
method: "frappe.core.doctype.data_import.data_import.get_importable_doc",
method: "frappe.core.doctype.data_import.data_import.get_importable_doctypes",
callback: function (r) {
let importable_doctypes = r.message;
frm.set_query("reference_doctype", function () {
return {
"filters": {
"issingle": 0,
"istable": 0,
"name": ['in', r.message]
"name": ['in', importable_doctypes]
}
};
});

View file

@ -30,9 +30,8 @@ class DataImport(Document):
@frappe.whitelist()
def get_importable_doc():
import_lst = frappe.cache().hget("can_import", frappe.session.user)
return import_lst
def get_importable_doctypes():
return frappe.cache().hget("can_import", frappe.session.user)
@frappe.whitelist()
def import_data(data_import):

View file

@ -19,6 +19,8 @@ from frappe.model import no_value_fields, table_fields
INVALID_VALUES = ["", None]
MAX_ROWS_IN_PREVIEW = 10
INSERT = "Insert New Records"
UPDATE = "Update Existing Records"
# pylint: disable=R0201
class Importer:
@ -34,9 +36,12 @@ class Importer:
if self.data_import.template_options:
template_options = frappe.parse_json(self.data_import.template_options)
self.template_options.update(template_options)
self.import_type = self.data_import.import_type
else:
self.data_import = None
self.import_type = self.import_type or INSERT
self.header_row = None
self.data = None
# used to store date formats guessed from data rows per column
@ -54,8 +59,10 @@ class Importer:
extension = None
if self.data_import and self.data_import.import_file:
file_doc = frappe.get_doc("File", {"file_url": self.data_import.import_file})
parts = file_doc.get_extension()
extension = parts[1]
content = file_doc.get_content()
extension = file_doc.file_name.split(".")[1]
extension = extension.lstrip(".")
if file_path:
content, extension = self.read_file(file_path)
@ -79,6 +86,12 @@ class Importer:
return file_content, extn
def read_content(self, content, extension):
error_title = _("Template Error")
if extension not in ("csv", "xlsx", "xls"):
frappe.throw(
_("Import template should be of type .csv, .xlsx or .xls"), title=error_title
)
if extension == "csv":
data = read_csv_content(content)
elif extension == "xlsx":
@ -86,6 +99,11 @@ class Importer:
elif extension == "xls":
data = read_xls_file_from_attached_file(content)
if len(data) <= 1:
frappe.throw(
_("Import template should contain a Header and atleast one row."), title=error_title
)
self.header_row = data[0]
self.data = data[1:]
@ -243,7 +261,7 @@ class Importer:
"fieldtype": "Data",
"fieldname": "name",
"label": "ID",
"reqd": self.data_import.import_type == "Update Existing Records",
"reqd": self.import_type == UPDATE,
"parent": doctype,
}
)
@ -589,8 +607,11 @@ class Importer:
return value
def parse_doc(doctype, docfields, values, row_number):
# new_doc returns a dict with default values set
doc = frappe.new_doc(doctype, as_dict=True)
doc = frappe._dict()
if self.import_type == INSERT:
# new_doc returns a dict with default values set
doc = frappe.new_doc(doctype, as_dict=True)
# remove standard fields and __islocal
for key in frappe.model.default_fields + ("__islocal",):
doc.pop(key, None)
@ -603,12 +624,46 @@ class Importer:
if value:
doc[df.fieldname] = self.parse_value(value, df)
is_table = frappe.get_meta(doctype).istable
is_update = self.import_type == UPDATE
if is_table and is_update and doc.get("name") in INVALID_VALUES:
# for table rows being inserted in update
# create a new doc with defaults set
new_doc = frappe.new_doc(doctype, as_dict=True)
new_doc.update(doc)
doc = new_doc
check_mandatory_fields(doctype, doc, row_number)
return doc
def check_mandatory_fields(doctype, doc, row_number):
# check if mandatory fields are set (except table fields)
"""If import type is Insert:
Check for mandatory fields (except table fields) in doc
if import type is Update:
Check for name field or autoname field in doc
"""
meta = frappe.get_meta(doctype)
if self.import_type == UPDATE:
if meta.istable:
# when updating records with table rows,
# there are two scenarios:
# 1. if row 'name' is provided in the template
# the table row will be updated
# 2. if row 'name' is not provided
# then a new row will be added
# so we dont need to check for mandatory
return
id_field = self.get_id_field(doctype)
if doc.get(id_field.fieldname) in INVALID_VALUES:
self.warnings.append(
{
"row": row_number,
"message": _("{0} is a mandatory field").format(id_field.label),
}
)
return
fields = [
df
for df in meta.fields
@ -685,21 +740,17 @@ class Importer:
)
elif mandatory_table_fields:
fields_string = ", ".join([df.label for df in mandatory_table_fields])
self.warnings.append(
{
"row": first_row[0],
"message": _("There should be atleast one row for the following tables: {0}").format(fields_string),
}
message = _("There should be atleast one row for the following tables: {0}").format(
fields_string
)
self.warnings.append({"row": first_row[0], "message": message})
return doc, rows, data[len(rows) :]
def process_doc(self, doc):
import_type = self.data_import.import_type
if import_type == "Insert New Records":
if self.import_type == INSERT:
return self.insert_record(doc)
elif import_type == "Update Existing Records":
elif self.import_type == UPDATE:
return self.update_record(doc)
def insert_record(self, doc):
@ -747,7 +798,7 @@ class Importer:
d.missing_values.remove(link_value)
def update_record(self, doc):
id_fieldname = self.get_id_fieldname()
id_fieldname = self.get_id_fieldname(self.doctype)
id_value = doc[id_fieldname]
existing_doc = frappe.get_doc(self.doctype, id_value)
existing_doc.flags.via_data_import = self.data_import.name
@ -804,12 +855,6 @@ class Importer:
df=col.df,
)
def get_id_fieldname(self):
autoname_field = self.get_autoname_field(self.doctype)
if autoname_field:
return autoname_field.fieldname
return "name"
def get_eta(self, current, total, processing_time):
remaining = total - current
eta = processing_time * remaining
@ -826,6 +871,15 @@ class Importer:
mandatory_fields_count += 1
return mandatory_fields_count == 1
def get_id_fieldname(self, doctype):
return self.get_id_field(doctype).fieldname
def get_id_field(self, doctype):
autoname_field = self.get_autoname_field(doctype)
if autoname_field:
return autoname_field
return frappe._dict({"label": "ID", "fieldname": "name", "fieldtype": "Data"})
def get_autoname_field(self, doctype):
meta = frappe.get_meta(doctype)
if meta.autoname and meta.autoname.startswith("field:"):
@ -862,15 +916,15 @@ class Importer:
if failed_records:
print("Failed to import {0} records".format(len(failed_records)))
file_name = '{0}_import_on_{1}.txt'.format(self.doctype, frappe.utils.now())
print('Check {0} for errors'.format(os.path.join('sites', file_name)))
file_name = "{0}_import_on_{1}.txt".format(self.doctype, frappe.utils.now())
print("Check {0} for errors".format(os.path.join("sites", file_name)))
text = ""
for w in failed_records:
text += "Row Indexes: {0}\n".format(str(w.get('row_indexes', [])))
text += "Messages:\n{0}\n".format('\n'.join(w.get('messages', [])))
text += "Traceback:\n{0}\n\n".format(w.get('exception'))
text += "Row Indexes: {0}\n".format(str(w.get("row_indexes", [])))
text += "Messages:\n{0}\n".format("\n".join(w.get("messages", [])))
text += "Traceback:\n{0}\n\n".format(w.get("exception"))
with open(file_name, 'w') as f:
with open(file_name, "w") as f:
f.write(text)

View file

@ -82,10 +82,9 @@ frappe.ui.form.on('Data Import Beta', {
() => frappe.set_route('List', frm.doc.reference_doctype));
}
frm.disable_save();
if (frm.doc.status !== 'Success') {
if (frm.import_in_progress) {
frm.disable_save();
} else if (!frm.is_new() && frm.doc.import_file) {
if (!frm.is_new() && frm.doc.import_file) {
let label = frm.doc.status === 'Pending' ? __('Start Import') : __('Retry');
frm.page.set_primary_action(label, () => frm.events.start_import(frm));
} else {
@ -323,13 +322,23 @@ frappe.ui.form.on('Data Import Beta', {
.map(log => {
let html = '';
if (log.success) {
html = __('Successfully imported {0}', [
`<span class="underline">${frappe.utils.get_form_link(
frm.doc.reference_doctype,
log.docname,
true
)}<span>`
]);
if (frm.doc.import_type === 'Insert New Records') {
html = __('Successfully imported {0}', [
`<span class="underline">${frappe.utils.get_form_link(
frm.doc.reference_doctype,
log.docname,
true
)}<span>`
]);
} else {
html = __('Successfully updated {0}', [
`<span class="underline">${frappe.utils.get_form_link(
frm.doc.reference_doctype,
log.docname,
true
)}<span>`
]);
}
} else {
let messages = log.messages
.map(JSON.parse)

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2013-02-18 13:36:19",
@ -28,6 +29,7 @@
"name_case",
"column_break_15",
"description",
"documentation",
"form_settings_section",
"image_field",
"timeline_field",
@ -57,6 +59,10 @@
"restrict_to_domain",
"read_only",
"in_create",
"actions_section",
"actions",
"links_section",
"links",
"web_view",
"has_web_view",
"allow_guest_to_view",
@ -454,11 +460,39 @@
"fieldname": "nsm_parent_field",
"fieldtype": "Data",
"label": "Parent Field (Tree)"
},
{
"description": "URL for documentation or help",
"fieldname": "documentation",
"fieldtype": "Data",
"label": "Documentation Link"
},
{
"fieldname": "actions_section",
"fieldtype": "Section Break",
"label": "Actions"
},
{
"fieldname": "actions",
"fieldtype": "Table",
"label": "Actions",
"options": "DocType Action"
},
{
"fieldname": "links_section",
"fieldtype": "Section Break",
"label": "Links Section"
},
{
"fieldname": "links",
"fieldtype": "Table",
"label": "Links",
"options": "DocType Link"
}
],
"icon": "fa fa-bolt",
"idx": 6,
"modified": "2019-09-07 14:28:05.392490",
"modified": "2019-11-25 17:24:03.690192",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -727,8 +727,8 @@ def validate_fields(meta):
if not options:
frappe.throw(_("{0}: Options must be a valid DocType for field {1} in row {2}").format(docname, d.label, d.idx), WrongOptionsDoctypeLinkError)
elif not (options == d.options):
frappe.throw(_("{0}: Options {1} must be the same as doctype name {2} for the field {3}", DoctypeLinkError)
.format(docname, d.options, options, d.label))
frappe.throw(_("{0}: Options {1} must be the same as doctype name {2} for the field {3}")
.format(docname, d.options, options, d.label), DoctypeLinkError)
else:
# fix case
d.options = options
@ -905,7 +905,7 @@ def validate_fields(meta):
def check_illegal_depends_on_conditions(docfield):
''' assignment operation should not be allowed in the depends on condition.'''
depends_on_fields = ["depends_on", "collapsible_depends_on"]
depends_on_fields = ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]
for field in depends_on_fields:
depends_on = docfield.get(field, None)
if depends_on and ("=" in depends_on) and \

View file

@ -96,14 +96,19 @@ class TestDocType(unittest.TestCase):
def test_all_depends_on_fields_conditions(self):
import re
docfields = frappe.get_all("DocField", or_filters={
docfields = frappe.get_all("DocField",
or_filters={
"ifnull(depends_on, '')": ("!=", ''),
"ifnull(collapsible_depends_on, '')": ("!=", '')
}, fields=["parent", "depends_on", "collapsible_depends_on", "fieldname", "fieldtype"])
"ifnull(collapsible_depends_on, '')": ("!=", ''),
"ifnull(mandatory_depends_on, '')": ("!=", ''),
"ifnull(read_only_depends_on, '')": ("!=", '')
},
fields=["parent", "depends_on", "collapsible_depends_on", "mandatory_depends_on",\
"read_only_depends_on", "fieldname", "fieldtype"])
pattern = """[\w\.:_]+\s*={1}\s*[\w\.@'"]+"""
for field in docfields:
for depends_on in ["depends_on", "collapsible_depends_on"]:
for depends_on in ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]:
condition = field.get(depends_on)
if condition:
self.assertFalse(re.match(pattern, condition))

View file

@ -0,0 +1,57 @@
{
"actions": [],
"creation": "2019-09-23 16:28:13.953520",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"label",
"action_type",
"action",
"group"
],
"fields": [
{
"columns": 2,
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"reqd": 1
},
{
"fieldname": "group",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Group"
},
{
"columns": 2,
"fieldname": "action_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Action Type",
"options": "Server Action",
"reqd": 1
},
{
"columns": 4,
"fieldname": "action",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Action",
"reqd": 1
}
],
"istable": 1,
"modified": "2019-09-24 09:11:39.860100",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType Action",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

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

View file

@ -0,0 +1,46 @@
{
"actions": [],
"creation": "2019-09-24 11:41:25.291377",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"link_doctype",
"link_fieldname",
"group"
],
"fields": [
{
"fieldname": "link_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Link DocType",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "link_fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Link Fieldname",
"reqd": 1
},
{
"fieldname": "group",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Group"
}
],
"istable": 1,
"modified": "2019-09-24 11:41:25.291377",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType Link",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

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

View file

@ -89,8 +89,9 @@ class File(Document):
def validate(self):
if self.is_new():
self.set_is_private()
self.set_file_name()
self.validate_duplicate_entry()
self.validate_file_name()
self.validate_folder()
if not self.file_url and not self.flags.ignore_file_validate:
@ -133,6 +134,9 @@ class File(Document):
frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
self.attached_to_field, self.file_url)
if self.file_url and (self.is_private != self.file_url.startswith('/private')):
frappe.throw(_('Invalid file URL. Please contact System Administrator.'))
def set_folder_name(self):
"""Make parent folders if not exists based on reference doctype and name"""
if self.attached_to_doctype and not self.folder:
@ -157,9 +161,11 @@ class File(Document):
def validate_duplicate_entry(self):
if not self.flags.ignore_duplicate_entry_error and not self.is_folder:
# check duplicate name
if not self.content_hash:
self.generate_content_hash()
# check duplicate assignement
# check duplicate name
# check duplicate assignment
filters = {
'content_hash': self.content_hash,
'is_private': self.is_private,
@ -184,21 +190,20 @@ class File(Document):
else:
self.file_url = duplicate_file.file_url
def validate_file_name(self):
def set_file_name(self):
if not self.file_name and self.file_url:
self.file_name = self.file_url.split('/')[-1]
def generate_content_hash(self):
if self.content_hash or not self.file_url:
if self.content_hash or not self.file_url or self.file_url.startswith('http'):
return
if self.file_url.startswith("/files/"):
try:
with open(get_files_path(self.file_name.lstrip("/")), "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
frappe.msgprint(_("File {0} does not exist").format(self.file_url))
raise
file_name = self.file_url.split('/')[-1]
try:
with open(get_files_path(file_name, is_private=self.is_private), "rb") as f:
self.content_hash = get_content_hash(f.read())
except IOError:
frappe.msgprint(_("File {0} does not exist").format(self.file_url))
raise
def on_trash(self):
if self.is_home_folder or self.is_attachments_folder:
@ -306,39 +311,6 @@ class File(Document):
exists = os.path.exists(self.get_full_path())
return exists
def upload(self):
# get record details
self.attached_to_doctype = frappe.form_dict.doctype
self.attached_to_name = frappe.form_dict.docname
self.attached_to_field = frappe.form_dict.docfield
self.file_url = frappe.form_dict.file_url
self.file_name = frappe.form_dict.filename
frappe.form_dict.is_private = cint(frappe.form_dict.is_private)
if not self.file_name and not self.file_url:
frappe.msgprint(_("Please select a file or url"),
raise_exception=True)
file_doc = self.get_file_doc()
comment = {}
if self.attached_to_doctype and self.attached_to_name:
comment = frappe.get_doc(self.attached_to_doctype, self.attached_to_name).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_content(self):
"""Returns [`file_name`, `content`] for given file name `fname`"""
@ -563,6 +535,9 @@ class File(Document):
except frappe.DoesNotExistError:
frappe.clear_messages()
def set_is_private(self):
if self.file_url:
self.is_private = cint(self.file_url.startswith('/private'))
def on_doctype_update():
frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])

View file

@ -25,7 +25,7 @@ class PreparedReport(Document):
enqueue(run_background, prepared_report=self.name, timeout=6000)
def on_trash(self):
remove_all("PreparedReport", self.name, from_delete=True)
remove_all("Prepared Report", self.name)
def run_background(prepared_report):
@ -85,7 +85,8 @@ def create_json_gz_file(data, dt, dn):
"file_name": json_filename,
"attached_to_doctype": dt,
"attached_to_name": dn,
"content": compressed_content
"content": compressed_content,
"is_private": 1
})
_file.save(ignore_permissions=True)

View file

@ -30,7 +30,7 @@ class Report(Document):
if self.is_standard == "No":
# allow only script manager to edit scripts
if frappe.session.user!="Administrator":
if self.report_type != 'Report Builder':
frappe.only_for('Script Manager', True)
if frappe.db.get_value("Report", self.name, "is_standard") == "Yes":

View file

@ -86,17 +86,28 @@ class TestReport(unittest.TestCase):
report = frappe.get_doc('Report', report_name)
report.report_script = '''
totals = {}
for user in frappe.get_all('User', fields = ['name', 'user_type', 'creation']):
if not user.user_type in totals:
totals[user.user_type] = 0
totals[user.user_type] = totals[user.user_type] + 1
data = [
[{'fieldname': 'name', 'label': 'ID'}],
[frappe.db.get_all('User', dict(user_type="System User"))]
[
{'fieldname': 'type', 'label': 'Type'},
{'fieldname': 'value', 'label': 'Value'}
],
[
{"type":key, "value": value} for key, value in totals.items()
]
]
'''
report.save()
data = report.get_data()
# check columns
self.assertEqual(data[0][0]['label'], 'ID')
self.assertEqual(data[0][0]['label'], 'Type')
# check values
self.assertTrue('Administrator' in [d.get('name') for d in data[1][0]])
self.assertTrue('System User' in [d.get('type') for d in data[1]])

View file

@ -0,0 +1,8 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Scheduled Job Log', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,64 @@
{
"actions": [],
"creation": "2019-09-23 14:36:36.935869",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"status",
"scheduled_job_type",
"details"
],
"fields": [
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Scheduled\nSuccess\nFailed",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "details",
"fieldtype": "Code",
"label": "Details",
"read_only": 1
},
{
"fieldname": "scheduled_job_type",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Scheduled Job",
"options": "Scheduled Job Type",
"read_only": 1,
"reqd": 1
}
],
"links": [],
"modified": "2019-09-25 11:55:10.646458",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Log",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

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

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestScheduledJobLog(unittest.TestCase):
pass

View file

@ -0,0 +1,8 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Scheduled Job Type', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,98 @@
{
"actions": [
{
"action": "frappe.core.doctype.scheduled_job_type.scheduled_job_type.execute_event",
"action_type": "Server Action",
"label": "Execute"
}
],
"creation": "2019-09-23 14:34:09.205368",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"stopped",
"method",
"frequency",
"cron_format",
"last_execution",
"create_log"
],
"fields": [
{
"fieldname": "method",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Method",
"read_only": 1,
"reqd": 1
},
{
"default": "0",
"fieldname": "stopped",
"fieldtype": "Check",
"label": "Stopped"
},
{
"default": "0",
"depends_on": "eval:doc.queue==='All'",
"fieldname": "create_log",
"fieldtype": "Check",
"label": "Create Log"
},
{
"fieldname": "last_execution",
"fieldtype": "Datetime",
"label": "Last Execution",
"read_only": 1
},
{
"allow_in_quick_entry": 1,
"depends_on": "eval:doc.queue==='Cron'",
"fieldname": "cron_format",
"fieldtype": "Data",
"label": "Cron Format",
"read_only": 1
},
{
"fieldname": "frequency",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Frequency",
"options": "All\nHourly\nHourly Long\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual",
"read_only": 1,
"reqd": 1
}
],
"in_create": 1,
"links": [
{
"link_doctype": "Scheduled Job Log",
"link_fieldname": "scheduled_job_type"
}
],
"modified": "2019-12-09 11:10:21.259929",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Type",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,156 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, json
from frappe.model.document import Document
from frappe.utils import now_datetime, get_datetime
from datetime import datetime
from croniter import croniter
from frappe.utils.background_jobs import enqueue, get_jobs
class ScheduledJobType(Document):
def autoname(self):
self.name = '.'.join(self.method.split('.')[-2:])
def validate(self):
if self.frequency != 'All':
# force logging for all events other than continuous ones (ALL)
self.create_log = 1
def enqueue(self):
# enqueue event if last execution is done
if self.is_event_due():
if frappe.flags.enqueued_jobs:
frappe.flags.enqueued_jobs.append(self.method)
if frappe.flags.execute_job:
self.execute()
else:
if not self.is_job_in_queue():
enqueue('frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job',
queue = self.get_queue_name(), job_type=self.method)
return True
return False
def is_event_due(self, current_time = None):
'''Return true if event is due based on time lapsed since last execution'''
# if the next scheduled event is before NOW, then its due!
return self.get_next_execution() <= (current_time or now_datetime())
def is_job_in_queue(self):
queued_jobs = get_jobs(site=frappe.local.site, key='job_type')[frappe.local.site]
return self.method in queued_jobs
def get_next_execution(self):
CRON_MAP = {
"Yearly": "0 0 1 1 *",
"Annual": "0 0 1 1 *",
"Monthly": "0 0 1 * *",
"Monthly Long": "0 0 1 * *",
"Weekly": "0 0 * * 0",
"Weekly Long": "0 0 * * 0",
"Daily": "0 0 * * *",
"Daily Long": "0 0 * * *",
"Hourly": "0 * * * *",
"Hourly Long": "0 * * * *",
"All": "0/" + str((frappe.get_conf().scheduler_interval or 240) // 60) + " * * * *",
}
if not self.cron_format:
self.cron_format = CRON_MAP[self.frequency]
return croniter(self.cron_format,
get_datetime(self.last_execution or datetime(2000, 1, 1))).get_next(datetime)
def execute(self):
self.scheduler_log = None
try:
self.log_status('Start')
frappe.get_attr(self.method)()
frappe.db.commit()
self.log_status('Complete')
except Exception:
frappe.db.rollback()
self.log_status('Failed')
def log_status(self, status):
# log file
frappe.logger(__name__).info('Scheduled Job {0}: {1} for {2}'.format(status, self.method, frappe.local.site))
self.update_scheduler_log(status)
def update_scheduler_log(self, status):
if not self.create_log:
return
if not self.scheduler_log:
self.scheduler_log = frappe.get_doc(dict(doctype = 'Scheduled Job Log', scheduled_job_type=self.name)).insert(ignore_permissions=True)
self.scheduler_log.db_set('status', status)
if status == 'Failed':
self.scheduler_log.db_set('details', frappe.get_traceback())
if status == 'Start':
self.db_set('last_execution', now_datetime(), update_modified=False)
frappe.db.commit()
def get_queue_name(self):
return 'long' if ('Long' in self.frequency) else 'default'
def on_trash(self):
frappe.db.sql('delete from `tabScheduled Job Log` where scheduled_job_type=%s', self.name)
@frappe.whitelist()
def execute_event(doc):
frappe.only_for('System Manager')
doc = json.loads(doc)
frappe.get_doc('Scheduled Job Type', doc.get('name')).enqueue()
def run_scheduled_job(job_type):
'''This is a wrapper function that runs a hooks.scheduler_events method'''
try:
frappe.get_doc('Scheduled Job Type', dict(method=job_type)).execute()
except Exception:
print(frappe.get_traceback())
def sync_jobs():
frappe.reload_doc('core', 'doctype', 'scheduled_job_type')
all_events = []
scheduler_events = frappe.get_hooks("scheduler_events")
insert_events(all_events, scheduler_events)
clear_events(all_events, scheduler_events)
def insert_events(all_events, scheduler_events):
for event_type in scheduler_events:
events = scheduler_events.get(event_type)
if isinstance(events, dict):
insert_cron_event(events, all_events)
else:
# hourly, daily etc
insert_event_list(events, event_type, all_events)
def insert_cron_event(events, all_events):
for cron_format in events:
for event in events.get(cron_format):
all_events.append(event)
insert_single_event('Cron', event, cron_format)
def insert_event_list(events, event_type, all_events):
for event in events:
all_events.append(event)
frequency = event_type.replace('_', ' ').title()
insert_single_event(frequency, event)
def insert_single_event(frequency, event, cron_format = None):
if not frappe.db.exists('Scheduled Job Type', dict(method=event)):
frappe.get_doc(dict(
doctype = 'Scheduled Job Type',
method = event,
cron_format = cron_format,
frequency = frequency
)).insert()
def clear_events(all_events, scheduler_events):
for event in frappe.get_all('Scheduled Job Type', ('name', 'method')):
if event.method not in all_events:
frappe.delete_doc('Scheduled Job Type', event.name)

View file

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import get_datetime
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
class TestScheduledJobType(unittest.TestCase):
def setUp(self):
if not frappe.get_all('Scheduled Job Type', limit=1):
frappe.db.rollback()
frappe.db.sql('truncate `tabScheduled Job Type`')
sync_jobs()
frappe.db.commit()
def test_sync_jobs(self):
all_job = frappe.get_doc('Scheduled Job Type',
dict(method='frappe.email.queue.flush'))
self.assertEqual(all_job.frequency, 'All')
daily_job = frappe.get_doc('Scheduled Job Type',
dict(method='frappe.email.queue.clear_outbox'))
self.assertEqual(daily_job.frequency, 'Daily')
# check if cron jobs are synced
cron_job = frappe.get_doc('Scheduled Job Type',
dict(method='frappe.oauth.delete_oauth2_data'))
self.assertEqual(cron_job.frequency, 'Cron')
self.assertEqual(cron_job.cron_format, '0/15 * * * *')
def test_daily_job(self):
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.queue.clear_outbox'))
job.db_set('last_execution', '2019-01-01 00:00:00')
self.assertTrue(job.is_event_due(get_datetime('2019-01-02 00:00:06')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:00:06')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-01 23:59:59')))
def test_weekly_job(self):
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.social.doctype.energy_point_log.energy_point_log.send_weekly_summary'))
job.db_set('last_execution', '2019-01-01 00:00:00')
self.assertTrue(job.is_event_due(get_datetime('2019-01-06 00:00:01')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-02 00:00:06')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-05 23:59:59')))
def test_monthly_job(self):
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.email.doctype.auto_email_report.auto_email_report.send_monthly'))
job.db_set('last_execution', '2019-01-01 00:00:00')
self.assertTrue(job.is_event_due(get_datetime('2019-02-01 00:00:01')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-15 00:00:06')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-31 23:59:59')))
def test_cron_job(self):
# runs every 15 mins
job = frappe.get_doc('Scheduled Job Type', dict(method = 'frappe.oauth.delete_oauth2_data'))
job.db_set('last_execution', '2019-01-01 00:00:00')
self.assertTrue(job.is_event_due(get_datetime('2019-01-01 00:15:01')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:05:06')))
self.assertFalse(job.is_event_due(get_datetime('2019-01-01 00:14:59')))

View file

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2019-09-30 11:56:57.943241",
"doctype": "DocType",
@ -43,7 +44,7 @@
"fieldname": "doctype_event",
"fieldtype": "Select",
"label": "DocType Event",
"options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete"
"options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
},
{
"depends_on": "eval:doc.script_type==='API'",
@ -73,7 +74,8 @@
"fieldtype": "Section Break"
}
],
"modified": "2019-10-09 15:08:40.085059",
"links": [],
"modified": "2019-12-17 12:55:07.389775",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",

View file

@ -14,6 +14,8 @@ EVENT_MAP = {
'on_cancel': 'After Cancel',
'on_trash': 'Before Delete',
'after_delete': 'After Delete',
'before_update_after_submit': 'Before Save (Submitted Document)',
'on_update_after_submit': 'After Save (Submitted Document)'
}
def run_server_script_api(method):
@ -56,8 +58,10 @@ def get_server_script_map():
script_map = frappe.cache().get_value('server_script_map')
if script_map is None:
script_map = {}
for script in frappe.get_all('Server Script', ('name', 'reference_doctype', 'doctype_event',
'api_method', 'script_type')):
enabled_server_scripts = frappe.get_all('Server Script',
fields=('name', 'reference_doctype', 'doctype_event','api_method', 'script_type'),
filters={'disabled': 0})
for script in enabled_server_scripts:
if script.script_type == 'DocType Event':
script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name)
else:

View file

@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2014-04-17 16:53:52.640856",
"doctype": "DocType",
"document_type": "System",
@ -13,6 +14,7 @@
"setup_complete",
"date_and_number_format",
"date_format",
"time_format",
"column_break_7",
"number_format",
"float_precision",
@ -21,7 +23,7 @@
"backup_limit",
"background_workers",
"enable_scheduler",
"scheduler_last_event",
"dormant_days",
"permissions",
"apply_strict_user_permissions",
"column_break_21",
@ -117,6 +119,14 @@
"options": "yyyy-mm-dd\ndd-mm-yyyy\ndd/mm/yyyy\ndd.mm.yyyy\nmm/dd/yyyy\nmm-dd-yyyy",
"reqd": 1
},
{
"default": "HH:mm:ss",
"fieldname": "time_format",
"fieldtype": "Select",
"label": "Time Format",
"options": "HH:mm:ss\nHH:mm",
"reqd": 1
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
@ -168,13 +178,6 @@
"hidden": 1,
"label": "Enable Scheduled Jobs"
},
{
"fieldname": "scheduler_last_event",
"fieldtype": "Data",
"hidden": 1,
"label": "Scheduler Last Event",
"report_hide": 1
},
{
"collapsible": 1,
"fieldname": "permissions",
@ -397,11 +400,18 @@
"fieldname": "allow_guests_to_upload_files",
"fieldtype": "Check",
"label": "Allow Guests to Upload Files"
},
{
"default": "4",
"description": "Will run scheduled jobs only once a day for inactive sites. Default 4 days if set to 0.",
"fieldname": "dormant_days",
"fieldtype": "Int",
"label": "Run Jobs only Daily if Inactive For (Days)"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"modified": "2019-08-16 08:26:45.936626",
"modified": "2019-09-24 10:04:28.807388",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
@ -419,4 +429,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
}

View file

@ -1,23 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Test Runner", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially('Test Runner', [
// insert a new Test Runner
() => frappe.tests.make([
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);
});

View file

@ -1,87 +0,0 @@
// Copyright (c) 2017, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Test Runner', {
refresh: (frm) => {
frm.disable_save();
frm.page.set_primary_action(__("Run Tests"), () => {
return new Promise(resolve => {
let wrapper = $(frm.fields_dict.output.wrapper).empty();
$("<p>Loading...</p>").appendTo(wrapper);
// all tests
frappe.call({
method: 'frappe.core.doctype.test_runner.test_runner.get_test_js',
args: { test_path: frm.doc.module_path }
}).always((data) => {
$("<div id='qunit'></div>").appendTo(wrapper.empty());
frm.events.run_tests(frm, data.message);
resolve();
});
});
});
},
run_tests: function(frm, files) {
frappe.flags.in_test = true;
let require_list = [
"assets/frappe/js/lib/jquery/qunit.js",
"assets/frappe/js/lib/jquery/qunit.css"
].concat();
frappe.require(require_list, () => {
files.forEach((f) => {
frappe.dom.eval(f.script);
});
QUnit.config.notrycatch = true;
window.onerror = function(msg, url, lineNo, columnNo, error) {
console.log(error.stack); // eslint-disable-line
$('<div id="frappe-qunit-done"></div>').appendTo($('body'));
};
QUnit.testDone(function(details) {
// var result = {
// "Module name": details.module,
// "Test name": details.name,
// "Assertions": {
// "Total": details.total,
// "Passed": details.passed,
// "Failed": details.failed
// },
// "Skipped": details.skipped,
// "Todo": details.todo,
// "Runtime": details.runtime
// };
// eslint-disable-next-line
// console.log(JSON.stringify(result, null, 2));
details.assertions.map(a => {
// eslint-disable-next-line
console.log(`${a.result ? '✔' : '✗'} ${a.message}`);
});
});
QUnit.load();
QUnit.done(({ total, failed, passed, runtime }) => {
// flag for selenium that test is done
console.log( `Total: ${total}, Failed: ${failed}, Passed: ${passed}, Runtime: ${runtime}` ); // eslint-disable-line
if(failed) {
console.log('Tests Failed'); // eslint-disable-line
} else {
console.log('Tests Passed'); // eslint-disable-line
}
frappe.set_route('Form', 'Test Runner', 'Test Runner');
$('<div id="frappe-qunit-done"></div>').appendTo($('body'));
});
});
}
});

View file

@ -1,152 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-06-26 10:57:19.976624",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "module_path",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Module Path",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "app",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "App",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "output",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Output",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-19 03:22:33.221169",
"modified_by": "Administrator",
"module": "Core",
"name": "Test Runner",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 0,
"role": "Administrator",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View file

@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, os
from frappe.model.document import Document
class TestRunner(Document):
pass
@frappe.whitelist()
def get_test_js(test_path=None):
'''Get test + data for app, example: app/tests/ui/test_name.js'''
if not test_path:
test_path = frappe.db.get_single_value('Test Runner', 'module_path')
test_js = []
# split
app, test_path = test_path.split(os.path.sep, 1)
# now full path
test_path = frappe.get_app_path(app, test_path)
def add_file(path):
with open(path, 'r') as fileobj:
test_js.append(dict(
script = fileobj.read()
))
# add test_lib.js
add_file(frappe.get_app_path('frappe', 'tests', 'ui', 'data', 'test_lib.js'))
add_file(test_path)
return test_js

View file

@ -8,6 +8,7 @@ from frappe.utils import cint, has_gravatar, format_datetime, now_datetime, get_
from frappe import throw, msgprint, _
from frappe.utils.password import update_password as _update_password
from frappe.desk.notifications import clear_notifications
from frappe.desk.doctype.notification_settings.notification_settings import create_notification_settings
from frappe.utils.user import get_system_managers
from bs4 import BeautifulSoup
import frappe.permissions
@ -46,6 +47,9 @@ class User(Document):
self.flags.in_insert = True
throttle_user_creation()
def after_insert(self):
create_notification_settings(self.name)
def validate(self):
self.check_demo()
@ -93,7 +97,9 @@ class User(Document):
self.share_with_self()
clear_notifications(user=self.name)
frappe.clear_cache(user=self.name)
self.send_password_notification(self.__new_password)
if self.__new_password:
self.send_password_notification(self.__new_password)
self.reset_password_key = ''
create_contact(self, ignore_mandatory=True)
if self.name not in ('Administrator', 'Guest') and not self.user_image:
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
@ -362,7 +368,10 @@ class User(Document):
(tab, field, '%s', field, '%s'), (new_name, old_name))
if frappe.db.exists("Chat Profile", old_name):
frappe.rename_doc("Chat Profile", old_name, new_name, force=True)
frappe.rename_doc("Chat Profile", old_name, new_name, force=True, show_alert=False)
if frappe.db.exists("Notification Settings", old_name):
frappe.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False)
# set email
frappe.db.sql("""UPDATE `tabUser`
@ -1064,4 +1073,4 @@ def generate_keys(user):
user_details.save()
return {"api_secret": api_secret}
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)

View file

@ -1,3 +1,7 @@
.version-info {
overflow: auto;
}
.version-info pre {
border: 0px;
margin: 0px;
@ -14,4 +18,4 @@
.version-info .danger {
background-color: #f2dede !important;
}
}

View file

@ -11,7 +11,7 @@
<tbody>
{% for j in jobs %}
<tr>
<td><span class="indicator {{ j.color }}" title="{{ j.status }}">{{ j.queue.split(".").slice(-1)[0] }}</span></td>
<td><span class="indicator {{ j.color }}" title="{{ j.get_status() }}">{{ j.queue.split(".").slice(-1)[0] }}</span></td>
<td style="overflow: auto;">
<div>
{{ frappe.utils.encode_tags(j.job_name) }}

View file

@ -29,9 +29,9 @@ def get_info(show_failed=False):
jobs.append({
'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \
or str(j.kwargs.get('job_name')),
'status': j.status, 'queue': name,
'status': j.get_status(), 'queue': name,
'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)),
'color': colors[j.status]
'color': colors[j.get_status()]
})
if j.exc_info:
jobs[-1]['exc_info'] = j.exc_info

View file

@ -36,13 +36,15 @@ def generate_and_cache_results(chart, chart_name, function, cache_key):
def get_from_date_from_timespan(to_date, timespan):
days = months = years = 0
if "Last Week" == timespan:
if timespan == "Last Week":
days = -7
if "Last Month" == timespan:
if timespan == "Last Month":
months = -1
elif "Last Quarter" == timespan:
elif timespan == "Last Quarter":
months = -3
elif "Last Year" == timespan:
elif timespan == "Last Year":
years = -1
elif timespan == "All Time":
years = -50
return add_to_date(to_date, years=years, months=months, days=days,
as_datetime=True)

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"creation": "2013-01-10 16:34:01",
"description": "Adds a custom field to a DocType",
@ -24,10 +25,8 @@
"collapsible_depends_on",
"default",
"depends_on",
"description",
"permlevel",
"width",
"columns",
"mandatory_depends_on",
"read_only_depends_on",
"properties",
"reqd",
"unique",
@ -46,7 +45,11 @@
"report_hide",
"search_index",
"ignore_xss_filter",
"translatable"
"translatable",
"description",
"permlevel",
"width",
"columns"
],
"fields": [
{
@ -349,11 +352,24 @@
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"length": 255
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"length": 255
}
],
"icon": "fa fa-glass",
"idx": 1,
"modified": "2019-09-11 12:57:19.268934",
"links": [],
"modified": "2019-12-12 21:31:08.209996",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",

View file

@ -59,6 +59,8 @@ docfield_properties = {
'report_hide': 'Check',
'allow_on_submit': 'Check',
'translatable': 'Check',
'mandatory_depends_on': 'Data',
'read_only_depends_on': 'Data',
'depends_on': 'Data',
'description': 'Text',
'default': 'Text',
@ -68,7 +70,8 @@ docfield_properties = {
'columns': 'Int',
'remember_last_selected_value': 'Check',
'allow_bulk_edit': 'Check',
'auto_repeat': 'Link'
'auto_repeat': 'Link',
'allow_in_quick_entry': 'Check'
}
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),

View file

@ -15,7 +15,7 @@ import frappe.model.meta
from frappe import _
from time import time
from frappe.utils import now, getdate, cast_fieldtype
from frappe.utils import now, getdate, cast_fieldtype, get_datetime
from frappe.utils.background_jobs import execute_job, get_queue
from frappe.model.utils.link_count import flush_local_link_count
from frappe.utils import cint
@ -941,6 +941,16 @@ class Database(object):
else:
frappe.throw(_('No conditions provided'))
def get_last_created(self, doctype):
last_record = self.get_all(doctype, ('creation'), limit=1, order_by='creation desc')
if last_record:
return get_datetime(last_record[0].creation)
else:
return None
def clear_table(self, doctype):
self.sql('truncate `tab{}`'.format(doctype))
def log_touched_tables(self, query, values=None):
if values:
query = frappe.safe_decode(self._cursor.mogrify(query, values))

View file

@ -80,12 +80,14 @@ class DbManager:
if pipe:
print('Creating Database...')
command = '{pipe} mysql -u {user} -p{password} -h{host} {target} {source}'.format(
command = '{pipe} mysql -u {user} -p{password} -h{host} ' + ('-P{port}' if frappe.db.port else '') + ' {target} {source}'
command = command.format(
pipe=pipe,
user=esc(user),
password=esc(password),
host=esc(frappe.db.host),
target=esc(target),
source=source
source=source,
port=frappe.db.port
)
os.system(command)

View file

@ -40,6 +40,8 @@ CREATE TABLE `tabDocField` (
`show_preview_popup` int(1) NOT NULL DEFAULT 0,
`trigger` varchar(255) DEFAULT NULL,
`collapsible_depends_on` text,
`mandatory_depends_on` text,
`read_only_depends_on` text,
`depends_on` text,
`permlevel` int(11) NOT NULL DEFAULT 0,
`ignore_user_permissions` int(1) NOT NULL DEFAULT 0,
@ -105,6 +107,53 @@ CREATE TABLE `tabDocPerm` (
KEY `parent` (`parent`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `tabDocType Action`
--
CREATE TABLE `tabDocType Action` (
`name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL,
`creation` datetime(6) DEFAULT NULL,
`modified` datetime(6) DEFAULT NULL,
`modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`docstatus` int(1) NOT NULL DEFAULT 0,
`parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`idx` int(8) NOT NULL DEFAULT 0,
`label` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`action_type` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`action` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `modified` (`modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
--
-- Table structure for table `tabDocType Action`
--
CREATE TABLE `tabDocType Link` (
`name` varchar(140) COLLATE utf8mb4_unicode_ci NOT NULL,
`creation` datetime(6) DEFAULT NULL,
`modified` datetime(6) DEFAULT NULL,
`modified_by` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`owner` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`docstatus` int(1) NOT NULL DEFAULT 0,
`parent` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`parentfield` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`parenttype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`idx` int(8) NOT NULL DEFAULT 0,
`group` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`link_doctype` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`link_fieldname` varchar(140) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `modified` (`modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED;
--
-- Table structure for table `tabDocType`
--

View file

@ -107,7 +107,7 @@ class PostgresDatabase(Database):
from information_schema.tables
where table_catalog='{0}'
and table_type = 'BASE TABLE'
and table_schema='public'""".format(frappe.conf.db_name))]
and table_schema='{1}'""".format(frappe.conf.db_name, frappe.conf.get("db_schema", "public")))]
def format_date(self, date):
if not date:

View file

@ -40,6 +40,8 @@ CREATE TABLE "tabDocField" (
"show_preview_popup" smallint NOT NULL DEFAULT 0,
"trigger" varchar(255) DEFAULT NULL,
"collapsible_depends_on" text,
"mandatory_depends_on" text,
"read_only_depends_on" text,
"depends_on" text,
"permlevel" bigint NOT NULL DEFAULT 0,
"ignore_user_permissions" smallint NOT NULL DEFAULT 0,
@ -106,6 +108,57 @@ CREATE TABLE "tabDocPerm" (
create index on "tabDocPerm" ("parent");
--
-- Table structure for table "tabDocType Action"
--
DROP TABLE IF EXISTS "tabDocType Action";
CREATE TABLE "tabDocType Action" (
"name" varchar(255) NOT NULL,
"creation" timestamp(6) DEFAULT NULL,
"modified" timestamp(6) DEFAULT NULL,
"modified_by" varchar(255) DEFAULT NULL,
"owner" varchar(255) DEFAULT NULL,
"docstatus" smallint NOT NULL DEFAULT 0,
"parent" varchar(255) DEFAULT NULL,
"parentfield" varchar(255) DEFAULT NULL,
"parenttype" varchar(255) DEFAULT NULL,
"idx" bigint NOT NULL DEFAULT 0,
"label" varchar(140) NOT NULL,
"group" varchar(140) DEFAULT NULL,
"action_type" varchar(140) NOT NULL,
"action" varchar(140) NOT NULL,
PRIMARY KEY ("name")
) ;
create index on "tabDocType Action" ("parent");
--
-- Table structure for table "tabDocType Link"
--
DROP TABLE IF EXISTS "tabDocType Link";
CREATE TABLE "tabDocType Link" (
"name" varchar(255) NOT NULL,
"creation" timestamp(6) DEFAULT NULL,
"modified" timestamp(6) DEFAULT NULL,
"modified_by" varchar(255) DEFAULT NULL,
"owner" varchar(255) DEFAULT NULL,
"docstatus" smallint NOT NULL DEFAULT 0,
"parent" varchar(255) DEFAULT NULL,
"parentfield" varchar(255) DEFAULT NULL,
"parenttype" varchar(255) DEFAULT NULL,
"idx" bigint NOT NULL DEFAULT 0,
"label" varchar(140) DEFAULT NULL,
"group" varchar(140) DEFAULT NULL,
"link_doctype" varchar(140) NOT NULL,
"link_fieldname" varchar(140) NOT NULL,
PRIMARY KEY ("name")
) ;
create index on "tabDocType Link" ("parent");
--
-- Table structure for table "tabDocType"
--

View file

@ -3,6 +3,15 @@
frappe.ui.form.on('Bulk Update', {
refresh: function(frm) {
frm.set_query("document_type", function() {
return {
filters: [
['DocType', 'issingle', '=', 0],
['DocType', 'name', 'not in', frappe.model.core_doctypes_list]
]
};
});
frm.page.set_primary_action(__('Update'), function() {
if (!frm.doc.update_value) {
frappe.throw(__('Field "value" is mandatory. Please specify value to be updated'));

View file

@ -41,6 +41,7 @@ frappe.ui.form.on('Dashboard Chart', {
timespan: function(frm) {
const time_interval_options = {
"Select Date Range": ["Quarterly", "Monthly", "Weekly", "Daily"],
"All Time": ["Yearly", "Monthly"],
"Last Year": ["Quarterly", "Monthly", "Weekly", "Daily"],
"Last Quarter": ["Monthly", "Weekly", "Daily"],
"Last Month": ["Weekly", "Daily"],

View file

@ -82,14 +82,14 @@
"fieldname": "timespan",
"fieldtype": "Select",
"label": "Timespan",
"options": "Last Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range"
"options": "All Time\nLast Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range"
},
{
"depends_on": "timeseries",
"fieldname": "time_interval",
"fieldtype": "Select",
"label": "Time Interval",
"options": "Quarterly\nMonthly\nWeekly\nDaily"
"options": "Yearly\nQuarterly\nMonthly\nWeekly\nDaily"
},
{
"default": "0",
@ -187,7 +187,7 @@
"label": "To Date"
}
],
"modified": "2019-11-04 12:32:14.525409",
"modified": "2019-11-18 16:20:11.529496",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",

View file

@ -74,8 +74,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
result = convert_to_dates(data, timegrain)
# add missing data points for periods where there was no result
result = add_missing_values(result, timegrain, from_date, to_date)
result = add_missing_values(result, timegrain, timespan, from_date, to_date)
chart_config = {
"labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
"datasets": [{
@ -133,7 +132,9 @@ def get_aggregate_function(chart_type):
"Average": "AVG",
}[chart_type]
def convert_to_dates(data, timegrain):
""" Converts individual dates within data to the end of period """
result = []
for d in data:
if timegrain == 'Daily':
@ -141,10 +142,11 @@ def convert_to_dates(data, timegrain):
elif timegrain == 'Weekly':
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), weeks = d[1] + 1), days = -1), d[2]])
elif timegrain == 'Monthly':
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months = d[1]), days = -1), d[2]])
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1]), days = -1), d[2]])
elif timegrain == 'Quarterly':
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months = d[1] * 3), days = -1), d[2]])
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1] * 3), days = -1), d[2]])
elif timegrain == 'Yearly':
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=12), days = -1), d[2]])
result[-1][0] = getdate(result[-1][0])
return result
@ -164,17 +166,17 @@ def get_unit_function(datefield, timegrain):
return unit_function
def add_missing_values(data, timegrain, from_date, to_date):
def add_missing_values(data, timegrain, timespan, from_date, to_date):
# add missing intervals
result = []
first_expected_date = get_period_ending(from_date, timegrain)
# fill out data before the first data point
first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1))
while first_data_point_date > first_expected_date:
result.append([first_expected_date, 0.0])
first_expected_date = get_next_expected_date(first_expected_date, timegrain)
if timespan != 'All Time':
first_expected_date = get_period_ending(from_date, timegrain)
# fill out data before the first data point
first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1))
while first_data_point_date > first_expected_date:
result.append([first_expected_date, 0.0])
first_expected_date = get_next_expected_date(first_expected_date, timegrain)
# fill data points and missing points
for i, d in enumerate(data):
@ -212,14 +214,16 @@ def get_next_expected_date(date, timegrain):
def get_period_ending(date, timegrain):
date = getdate(date)
if timegrain=='Daily':
if timegrain == 'Daily':
pass
elif timegrain=='Weekly':
elif timegrain == 'Weekly':
date = get_week_ending(date)
elif timegrain=='Monthly':
elif timegrain == 'Monthly':
date = get_month_ending(date)
elif timegrain=='Quarterly':
elif timegrain == 'Quarterly':
date = get_quarter_ending(date)
elif timegrain == 'Yearly':
date = get_year_ending(date)
return getdate(date)
@ -231,7 +235,7 @@ def get_week_ending(date):
# first day of next week
date = add_to_date('{}-01-01'.format(date.year), weeks = week_of_the_year + 1)
# last day of this week
return add_to_date(date, days = -1)
return add_to_date(date, days=-1)
def get_month_ending(date):
month_of_the_year = int(date.strftime('%m'))
@ -239,7 +243,7 @@ def get_month_ending(date):
date = add_to_date('{}-01-01'.format(date.year), months = month_of_the_year)
# last day of this month
return add_to_date(date, days = -1)
return add_to_date(date, days=-1)
def get_quarter_ending(date):
date = getdate(date)
@ -255,8 +259,17 @@ def get_quarter_ending(date):
return date
def get_year_ending(date):
''' returns year ending of the given date '''
# first day of next year (note year starts from 1)
date = add_to_date('{}-01-01'.format(date.year), months = 12)
# last day of this month
return add_to_date(date, days=-1)
class DashboardChart(Document):
def on_update(self):
frappe.cache().delete_key('chart-data:{}'.format(self.name))

View file

@ -36,6 +36,9 @@ class TestDashboardChart(unittest.TestCase):
self.assertEqual(get_period_ending('2019-10-01', 'Quarterly'),
getdate('2019-12-31'))
self.assertEqual(get_period_ending('2019-10-01', 'Yearly'),
getdate('2019-12-31'))
def test_dashboard_chart(self):
if frappe.db.exists('Dashboard Chart', 'Test Dashboard Chart'):
frappe.delete_doc('Dashboard Chart', 'Test Dashboard Chart')

View file

@ -29,12 +29,16 @@ class GlobalSearchSettings(Document):
repeated_dts = (", ".join([frappe.bold(dt) for dt in repeated_dts]))
frappe.throw(_("Document Type {0} has been repeated.").format(repeated_dts))
def get_doctypes_for_global_search():
doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC")
if not doctypes:
return []
# reset cache
frappe.cache().hdel('global_search', 'search_priorities')
def get_doctypes_for_global_search():
def get_from_db():
doctypes = frappe.get_list("Global Search DocType", fields=["document_type"], order_by="idx ASC")
return [d.document_type for d in doctypes] or []
return frappe.cache().hget("global_search", "search_priorities", get_from_db)
return [d.document_type for d in doctypes]
@frappe.whitelist()
def reset_global_search_settings_doctypes():
@ -57,7 +61,7 @@ def update_global_search_doctypes():
if search_doctypes.get(domain):
global_search_doctypes.extend(search_doctypes.get(domain))
doctype_list = set([dt.name for dt in frappe.get_list("DocType")])
doctype_list = set([dt.name for dt in frappe.get_all("DocType")])
allowed_in_global_search = []
for dt in global_search_doctypes:

View file

@ -10,7 +10,7 @@
"email_content",
"column_break_4",
"document_type",
"seen",
"read",
"document_name",
"from_user"
],
@ -57,14 +57,6 @@
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"fieldname": "seen",
"fieldtype": "Check",
"hidden": 1,
"ignore_user_permissions": 1,
"label": "Seen"
},
{
"fieldname": "document_name",
"fieldtype": "Data",
@ -79,11 +71,19 @@
"options": "User",
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"fieldname": "read",
"fieldtype": "Check",
"hidden": 1,
"ignore_user_permissions": 1,
"label": "Read"
}
],
"in_create": 1,
"modified": "2019-10-23 12:48:01.119356",
"modified_by": "Administrator",
"modified": "2019-11-12 15:22:35.283678",
"modified_by": "umair@erpnext.com",
"module": "Desk",
"name": "Notification Log",
"owner": "Administrator",

View file

@ -7,11 +7,12 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.desk.doctype.notification_settings.notification_settings import (is_notifications_enabled,
is_email_notifications_enabled, is_email_notifications_enabled_for_type)
is_email_notifications_enabled, is_email_notifications_enabled_for_type, set_seen_value)
class NotificationLog(Document):
def after_insert(self):
frappe.publish_realtime('notification', after_commit=True, user=self.for_user)
set_notifications_as_unseen(self.for_user)
if is_email_notifications_enabled(self.for_user):
send_notification_email(self)
@ -41,7 +42,6 @@ def enqueue_create_notification(users, doc):
This breaks new site creation if Redis server is not running.
We do not need any notifications in fresh installation
'''
if frappe.flags.in_install:
return
@ -64,13 +64,13 @@ def make_notification_logs(doc, users):
if is_notifications_enabled(user):
if doc.type == 'Energy Point' and not is_energy_point_enabled():
return
else:
_doc = frappe.new_doc('Notification Log')
_doc.update(doc)
_doc.for_user = user
_doc.subject = _doc.subject.replace('<div>', '').replace('</div>', '')
if _doc.for_user != _doc.from_user or doc.type == 'Energy Point':
_doc.insert(ignore_permissions=True)
_doc = frappe.new_doc('Notification Log')
_doc.update(doc)
_doc.for_user = user
_doc.subject = _doc.subject.replace('<div>', '').replace('</div>', '')
if _doc.for_user != _doc.from_user or doc.type == 'Energy Point':
_doc.insert(ignore_permissions=True)
def send_notification_email(doc):
is_type_enabled = is_email_notifications_enabled_for_type(doc.for_user, doc.type)
@ -112,11 +112,25 @@ def get_email_header(doc):
@frappe.whitelist()
def mark_as_seen(docname):
if docname:
frappe.db.set_value('Notification Log', docname, 'seen', 1, update_modified=False)
def mark_all_as_read():
unread_docs_list = frappe.db.get_all('Notification Log', filters = {'read': 0, 'for_user': frappe.session.user})
unread_docnames = [doc.name for doc in unread_docs_list]
if unread_docnames:
filters = {'name': ['in', unread_docnames]}
frappe.db.set_value('Notification Log', filters, 'read', 1, update_modified=False)
@frappe.whitelist()
def mark_as_read(docname):
if docname:
frappe.db.set_value('Notification Log', docname, 'read', 1, update_modified=False)
@frappe.whitelist()
def trigger_indicator_hide():
frappe.publish_realtime('indicator_hide', user=frappe.session.user)
def set_notifications_as_unseen(user):
try:
frappe.db.set_value('Notification Settings', user, 'seen', 0)
except frappe.DoesNotExistError:
return

View file

@ -0,0 +1,12 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Notification Settings', {
onload: () => {
frappe.breadcrumbs.add({
label: __('Settings'),
route: '#modules/Settings',
type: 'Custom'
});
}
});

View file

@ -13,7 +13,8 @@
"enable_email_assignment",
"enable_email_energy_point",
"enable_email_share",
"user"
"user",
"seen"
],
"fields": [
{
@ -72,14 +73,20 @@
"fieldname": "user",
"fieldtype": "Link",
"hidden": 1,
"in_list_view": 1,
"label": "User",
"options": "User",
"read_only": 1
},
{
"default": "0",
"fieldname": "seen",
"fieldtype": "Check",
"hidden": 1,
"label": "Seen"
}
],
"in_create": 1,
"modified": "2019-10-23 12:42:56.175928",
"modified": "2019-11-19 12:57:59.356786",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Settings",

View file

@ -31,12 +31,12 @@ def is_email_notifications_enabled_for_type(user, notification_type):
return True
return enabled
@frappe.whitelist()
def create_notification_settings():
_doc = frappe.new_doc('Notification Settings')
_doc.name = frappe.session.user
_doc.insert(ignore_permissions=True)
frappe.db.commit()
def create_notification_settings(user):
if not frappe.db.exists("Notification Settings", user):
_doc = frappe.new_doc('Notification Settings')
_doc.name = user
_doc.insert(ignore_permissions=True)
frappe.db.commit()
@frappe.whitelist()
@ -60,3 +60,7 @@ def get_permission_query_conditions(user):
if not user: user = frappe.session.user
return '''(`tabNotification Settings`.user = '{user}')'''.format(user=user)
@frappe.whitelist()
def set_seen_value(value, user):
frappe.db.set_value('Notification Settings', user, 'seen', value, update_modified=False)

View file

@ -0,0 +1,45 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Onboarding Slide', {
refresh: function(frm) {
frm.toggle_reqd('ref_doctype', (frm.doc.slide_type=='Create' || frm.doc.slide_type=='Settings'));
frm.toggle_reqd('slide_module', (frm.doc.slide_type=='Information' || frm.doc.slide_type=='Continue'));
},
ref_doctype: function(frm) {
frm.set_query('ref_doctype', function() {
if (frm.doc.slide_type === 'Create') {
return {
filters: {
'issingle': 0,
'istable': 0
}
};
} else if (frm.doc.slide_type === 'Settings') {
return {
filters: {
'issingle': 1,
'istable': 0
}
};
}
});
//fetch mandatory fields automatically
if (frm.doc.ref_doctype) {
frappe.model.clear_table(frm.doc, 'slide_fields');
let fields = frappe.meta.get_docfields(frm.doc.ref_doctype, null, {
reqd: 1
});
$.each(fields, function(_i, data) {
let row = frappe.model.add_child(frm.doc, 'Onboarding Slide', 'slide_fields');
row.label = data.label;
row.fieldtype = data.fieldtype;
row.fieldname = data.fieldname;
row.options = data.options;
});
refresh_field('slide_fields');
}
}
});

View file

@ -0,0 +1,184 @@
{
"autoname": "field:slide_title",
"creation": "2019-11-13 14:39:56.834658",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"slide_title",
"app",
"slide_order",
"column_break_4",
"image_src",
"slide_module",
"description_section_break",
"slide_desc",
"action_section_break",
"slide_type",
"column_break_6",
"max_count",
"add_more_button",
"section_break_18",
"ref_doctype",
"slide_fields",
"section_break_10",
"domains",
"column_break_12",
"help_links",
"is_completed"
],
"fields": [
{
"fieldname": "slide_title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Slide Title",
"reqd": 1,
"unique": 1
},
{
"fieldname": "slide_desc",
"fieldtype": "HTML Editor",
"label": "Slide Description"
},
{
"default": "3",
"depends_on": "add_more_button",
"description": "The amount of times you want to repeat the set of fields (eg: if you want 3 customers in the slide, set this field to 3. Only the first set of fields is shown as mandatory in the slide)",
"fieldname": "max_count",
"fieldtype": "Int",
"label": "Max Count"
},
{
"default": "0",
"depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
"fieldname": "add_more_button",
"fieldtype": "Check",
"label": "Add More Button"
},
{
"depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
"fieldname": "slide_fields",
"fieldtype": "Table",
"label": "Slide Fields",
"options": "Onboarding Slide Field"
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"description": "Specify in what all domains should the slides show up. If nothing is specified the slide is shown in all domains by default.",
"fieldname": "domains",
"fieldtype": "Table",
"label": "Domains",
"options": "Has Domain"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"description": "Add a help video link just in case user has no idea about what to fill in the slide.",
"fieldname": "help_links",
"fieldtype": "Table",
"label": "Help Links",
"options": "Onboarding Slide Help Link"
},
{
"fieldname": "action_section_break",
"fieldtype": "Section Break",
"label": "Action Settings"
},
{
"description": "If Slide Type is Create or Settings there should be a 'create_onboarding_docs' method in the {ref_doctype}.py file bound to be executed after the slide is completed.",
"fieldname": "slide_type",
"fieldtype": "Select",
"label": "Slide Type",
"options": "Information\nCreate\nSettings\nContinue",
"reqd": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "app",
"fieldtype": "Select",
"label": "App",
"options": "Frappe\nERPNext",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "image_src",
"fieldtype": "Data",
"label": "Slide Image Source"
},
{
"fieldname": "description_section_break",
"fieldtype": "Section Break",
"label": "Description"
},
{
"depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
"fieldname": "ref_doctype",
"fieldtype": "Link",
"label": "Reference Document Type",
"options": "DocType"
},
{
"default": "0",
"description": "Determines the order of the slide in the wizard. If the slide is not to be displayed, priority should be set to 0.",
"fieldname": "slide_order",
"fieldtype": "Int",
"label": "Slide Order"
},
{
"depends_on": "eval:doc.slide_type=='Information' || doc.slide_type=='Continue'",
"fieldname": "slide_module",
"fieldtype": "Link",
"label": "Module",
"options": "Module Def"
},
{
"collapsible_depends_on": "eval:doc.slide_type=='Create' || doc.slide_type=='Settings'",
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"label": "Fields"
},
{
"default": "0",
"fieldname": "is_completed",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Completed",
"print_hide": 1
}
],
"modified": "2019-12-04 10:50:43.528901",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Slide",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe import _
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
class OnboardingSlide(Document):
def validate(self):
if self.slide_type == 'Continue' and frappe.db.exists('Onboarding Slide', {'slide_type': 'Continue', 'name': ('!=', self.name)}):
frappe.throw(_('An Onboarding Slide of Slide Type Continue already exists.'))
if self.slide_order:
same_order_slide = frappe.db.exists('Onboarding Slide', {'slide_order': self.slide_order, 'name': ('!=', self.name)})
if same_order_slide:
frappe.throw(_('An Onboarding Slide <b>{0}</b> with the same slide order already exists').format(same_order_slide))
def on_update(self):
if self.ref_doctype:
module = frappe.db.get_value('DocType', self.ref_doctype, 'module')
else:
module = self.slide_module
export_to_files(record_list=[['Onboarding Slide', self.name]], record_module=module)
def get_onboarding_slides_as_list():
slides = []
slide_docs = frappe.db.get_all('Onboarding Slide',
filters={'is_completed': 0},
or_filters={'slide_order': ('!=', 0), 'slide_type': 'Continue'},
order_by='slide_order')
# to check if continue slide is required
first_slide = get_first_slide()
for entry in slide_docs:
# using get_doc because child table fields are not fetched in get_all
slide_doc = frappe.get_doc('Onboarding Slide', entry.name)
if frappe.scrub(slide_doc.app) in frappe.get_installed_apps():
slide = frappe._dict(
slide_type=slide_doc.slide_type,
title=slide_doc.slide_title,
help=slide_doc.slide_desc,
fields=slide_doc.slide_fields,
help_links=get_help_links(slide_doc),
add_more=slide_doc.add_more_button,
max_count=slide_doc.max_count,
image_src=get_slide_image(slide_doc),
ref_doctype=slide_doc.ref_doctype,
app=slide_doc.app
)
if slide.slide_type == 'Continue':
if is_continue_slide_required(first_slide):
slides.insert(0, slide)
else:
slides.append(slide)
return slides
@frappe.whitelist()
def get_onboarding_slides():
slides = []
slide_list = get_onboarding_slides_as_list()
active_domains = frappe.get_active_domains()
for slide in slide_list:
if not slide.domains or any(domain in active_domains for domain in slide.domains):
slides.append(slide)
return slides
def get_help_links(slide_doc):
links=[]
for link in slide_doc.help_links:
links.append({
'label': link.label,
'video_id': link.video_id
})
return links
def get_slide_image(slide_doc):
if slide_doc.image_src:
return slide_doc.image_src
return None
def is_continue_slide_required(first_slide):
# check if first slide itself is not completed
if not first_slide.is_completed:
return False
# check if there is any active slide which is not completed
return frappe.db.exists('Onboarding Slide', {
'is_completed': 0,
'slide_order': ('!=', 0),
'slide_type': ('!=', 'Continue')
})
@frappe.whitelist()
def create_onboarding_docs(values, doctype=None, app=None, slide_type=None):
data = json.loads(values)
doc = frappe.new_doc(doctype)
if hasattr(doc, 'create_onboarding_docs'):
doc.create_onboarding_docs(data)
else:
create_generic_onboarding_doc(data, doctype, slide_type)
def create_generic_onboarding_doc(data, doctype, slide_type):
if slide_type == 'Settings':
doc = frappe.get_single(doctype)
for entry in data:
doc.set(entry, data.get(entry))
doc.save()
elif slide_type == 'Create':
doc = frappe.new_doc(doctype)
for entry in data:
doc.set(entry, data.get(entry))
doc.flags.ignore_mandatory = True
doc.flags.ignore_links = True
doc.insert()
@frappe.whitelist()
def mark_slide_as_completed(slide_title):
frappe.db.set_value('Onboarding Slide', slide_title, 'is_completed', 1)
def get_first_slide():
slides = frappe.db.get_all('Onboarding Slide',
filters={'slide_order': ('!=', 0), 'slide_type': ('!=', 'Continue')},
order_by='slide_order',
fields=['name', 'is_completed']
)
return slides[0]

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestOnboardingSlide(unittest.TestCase):
pass

View file

@ -0,0 +1,74 @@
{
"creation": "2019-11-13 13:35:08.617909",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"label",
"fieldtype",
"fieldname",
"align",
"placeholder",
"reqd",
"column_break_4",
"options"
],
"fields": [
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label"
},
{
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Fieldtype",
"options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nFloat\nHTML\nInt\nRating\nSelect\nLink\nSmall Text\nText\nText Editor\nSection Break\nColumn Break"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname"
},
{
"fieldname": "align",
"fieldtype": "Select",
"label": "Align",
"options": "\ncenter\nleft\nright"
},
{
"fieldname": "placeholder",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Placeholder"
},
{
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"label": "Mandatory"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "options",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Options"
}
],
"istable": 1,
"modified": "2019-12-02 16:43:51.930018",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Slide Field",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View file

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

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