Merge branch 'develop' of https://github.com/frappe/frappe into force_listview_columns
This commit is contained in:
commit
729cada2fd
305 changed files with 6773 additions and 10169 deletions
2
.codacy.yml
Normal file
2
.codacy.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
exclude_paths:
|
||||
- '**.sql'
|
||||
2
.pylintrc
Normal file
2
.pylintrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
disable=access-member-before-definition
|
||||
disable=no-member
|
||||
40
.travis.yml
40
.travis.yml
|
|
@ -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
|
||||
|
||||
|
|
|
|||
53
cypress/fixtures/custom_submittable_doctype.js
Normal file
53
cypress/fixtures/custom_submittable_doctype.js
Normal 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
|
||||
};
|
||||
48
cypress/fixtures/datetime_doctype.js
Normal file
48
cypress/fixtures/datetime_doctype.js
Normal 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
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
55
cypress/integration/control_barcode.js
Normal file
55
cypress/integration/control_barcode.js
Normal 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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
128
cypress/integration/datetime.js
Normal file
128
cypress/integration/datetime.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
63
cypress/integration/depends_on.js
Normal file
63
cypress/integration/depends_on.js
Normal 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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
51
cypress/integration/grid_pagination.js
Normal file
51
cypress/integration/grid_pagination.js
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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'];
|
||||
|
|
|
|||
40
cypress/integration/report_view.js
Normal file
40
cypress/integration/report_view.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -22,8 +22,7 @@
|
|||
"fieldtype": "Link",
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Online",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@
|
|||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Token",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ip_address",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
57
frappe/core/doctype/doctype_action/doctype_action.json
Normal file
57
frappe/core/doctype/doctype_action/doctype_action.json
Normal 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
|
||||
}
|
||||
10
frappe/core/doctype/doctype_action/doctype_action.py
Normal file
10
frappe/core/doctype/doctype_action/doctype_action.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class DocTypeAction(Document):
|
||||
pass
|
||||
46
frappe/core/doctype/doctype_link/doctype_link.json
Normal file
46
frappe/core/doctype/doctype_link/doctype_link.json
Normal 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
|
||||
}
|
||||
10
frappe/core/doctype/doctype_link/doctype_link.py
Normal file
10
frappe/core/doctype/doctype_link/doctype_link.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class DocTypeLink(Document):
|
||||
pass
|
||||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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]])
|
||||
|
||||
|
|
|
|||
0
frappe/core/doctype/scheduled_job_log/__init__.py
Normal file
0
frappe/core/doctype/scheduled_job_log/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Scheduled Job Log', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
64
frappe/core/doctype/scheduled_job_log/scheduled_job_log.json
Normal file
64
frappe/core/doctype/scheduled_job_log/scheduled_job_log.json
Normal 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
|
||||
}
|
||||
10
frappe/core/doctype/scheduled_job_log/scheduled_job_log.py
Normal file
10
frappe/core/doctype/scheduled_job_log/scheduled_job_log.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class ScheduledJobLog(Document):
|
||||
pass
|
||||
|
|
@ -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
|
||||
0
frappe/core/doctype/scheduled_job_type/__init__.py
Normal file
0
frappe/core/doctype/scheduled_job_type/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Scheduled Job Type', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
156
frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
Normal file
156
frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
Normal 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)
|
||||
|
|
@ -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')))
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -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'));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
--
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
--
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
0
frappe/desk/doctype/onboarding_slide/__init__.py
Normal file
0
frappe/desk/doctype/onboarding_slide/__init__.py
Normal file
45
frappe/desk/doctype/onboarding_slide/onboarding_slide.js
Normal file
45
frappe/desk/doctype/onboarding_slide/onboarding_slide.js
Normal 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');
|
||||
}
|
||||
}
|
||||
});
|
||||
184
frappe/desk/doctype/onboarding_slide/onboarding_slide.json
Normal file
184
frappe/desk/doctype/onboarding_slide/onboarding_slide.json
Normal 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
|
||||
}
|
||||
134
frappe/desk/doctype/onboarding_slide/onboarding_slide.py
Normal file
134
frappe/desk/doctype/onboarding_slide/onboarding_slide.py
Normal 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]
|
||||
|
|
@ -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
|
||||
0
frappe/desk/doctype/onboarding_slide_field/__init__.py
Normal file
0
frappe/desk/doctype/onboarding_slide_field/__init__.py
Normal 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"
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue