Merge branch 'develop' into phone_field_control
This commit is contained in:
commit
e86378f26f
453 changed files with 9726 additions and 8926 deletions
|
|
@ -148,6 +148,7 @@
|
|||
"context": true,
|
||||
"before": true,
|
||||
"beforeEach": true,
|
||||
"after": true,
|
||||
"qz": true,
|
||||
"localforage": true,
|
||||
"extend_cscript": true
|
||||
|
|
|
|||
2
.github/helper/documentation.py
vendored
2
.github/helper/documentation.py
vendored
|
|
@ -24,6 +24,8 @@ def docs_link_exists(body):
|
|||
parts = parsed_url.path.split('/')
|
||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
||||
return True
|
||||
if parsed_url.netloc in ["docs.erpnext.com", "frappeframework.com"]:
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
|
|
@ -17,7 +17,7 @@ if [ "$TYPE" == "server" ]; then
|
|||
fi
|
||||
|
||||
if [ "$DB" == "mariadb" ];then
|
||||
sudo apt install mariadb-client-10.3
|
||||
sudo apt update && sudo apt install mariadb-client-10.3
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'";
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'";
|
||||
|
||||
|
|
|
|||
2
.github/workflows/docker-release.yml
vendored
2
.github/workflows/docker-release.yml
vendored
|
|
@ -12,4 +12,4 @@ jobs:
|
|||
- name: curl
|
||||
run: |
|
||||
apk add curl bash
|
||||
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests
|
||||
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.CI_PAT }}" https://api.github.com/repos/frappe/frappe_docker/actions/workflows/build_stable.yml/dispatches -d '{"ref":"main"}'
|
||||
|
|
|
|||
21
.github/workflows/patch-mariadb-tests.yml
vendored
21
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -10,6 +10,7 @@ concurrency:
|
|||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
name: Patch Test
|
||||
|
||||
|
|
@ -31,6 +32,12 @@ jobs:
|
|||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
run: |
|
||||
|
|
@ -106,16 +113,14 @@ jobs:
|
|||
source env/bin/activate
|
||||
cd apps/frappe/
|
||||
git remote set-url upstream https://github.com/frappe/frappe.git
|
||||
git fetch --all --tags
|
||||
|
||||
taglist=$(git tag --sort version:refname | grep -v "beta")
|
||||
last_release=$(echo "$taglist" | tail -1 | cut -d . -f 1 | cut -c 2-)
|
||||
|
||||
for version in $(seq 12 "$last_release")
|
||||
for version in $(seq 12 13)
|
||||
do
|
||||
last_tag=$(echo "$taglist" | grep "v$version" | tail -1)
|
||||
echo "Updating to $last_tag"
|
||||
git checkout -q -f "$last_tag"
|
||||
echo "Updating to v$version"
|
||||
branch_name="version-$version-hotfix"
|
||||
git fetch --depth 1 upstream $branch_name:$branch_name
|
||||
|
||||
git checkout -q -f $branch_name
|
||||
pip install -q -r requirements.txt
|
||||
bench --site test_site migrate
|
||||
done
|
||||
|
|
|
|||
3
.github/workflows/server-mariadb-tests.yml
vendored
3
.github/workflows/server-mariadb-tests.yml
vendored
|
|
@ -14,6 +14,7 @@ concurrency:
|
|||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -128,4 +129,4 @@ jobs:
|
|||
fail_ci_if_error: true
|
||||
files: /home/runner/frappe-bench/sites/coverage.xml
|
||||
verbose: true
|
||||
flags: server
|
||||
flags: server
|
||||
|
|
|
|||
1
.github/workflows/server-postgres-tests.yml
vendored
1
.github/workflows/server-postgres-tests.yml
vendored
|
|
@ -13,6 +13,7 @@ concurrency:
|
|||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
1
.github/workflows/ui-tests.yml
vendored
1
.github/workflows/ui-tests.yml
vendored
|
|
@ -13,6 +13,7 @@ concurrency:
|
|||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,6 +11,7 @@ dist/
|
|||
frappe/docs/current
|
||||
frappe/public/dist
|
||||
.vscode
|
||||
.vs
|
||||
node_modules
|
||||
.kdev4/
|
||||
*.kdev4
|
||||
|
|
|
|||
24
CODEOWNERS
24
CODEOWNERS
|
|
@ -3,18 +3,18 @@
|
|||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
* @frappe/frappe-review-team
|
||||
templates/ @surajshetty3416
|
||||
www/ @surajshetty3416
|
||||
integrations/ @leela
|
||||
patches/ @surajshetty3416 @gavindsouza
|
||||
email/ @leela
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
* @frappe/frappe-review-team
|
||||
templates/ @surajshetty3416
|
||||
www/ @surajshetty3416
|
||||
integrations/ @leela
|
||||
patches/ @surajshetty3416 @gavindsouza
|
||||
email/ @leela
|
||||
event_streaming/ @ruchamahabal
|
||||
data_import* @netchampfaris
|
||||
core/ @surajshetty3416
|
||||
database @gavindsouza
|
||||
model @gavindsouza
|
||||
requirements.txt @gavindsouza
|
||||
query_builder/ @gavindsouza
|
||||
commands/ @gavindsouza
|
||||
requirements.txt @gavindsouza
|
||||
query_builder/ @gavindsouza
|
||||
commands/ @gavindsouza
|
||||
workspace @shariquerik
|
||||
|
|
|
|||
|
|
@ -11,6 +11,15 @@ coverage:
|
|||
threshold: 0.5%
|
||||
flags:
|
||||
- server
|
||||
patch:
|
||||
default: false
|
||||
server:
|
||||
target: 85%
|
||||
threshold: 0%
|
||||
only_pulls: true
|
||||
if_ci_failed: ignore
|
||||
flags:
|
||||
- server
|
||||
|
||||
comment:
|
||||
layout: "diff, flags"
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@
|
|||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 20000,
|
||||
"pageLoadTimeout": 15000,
|
||||
"video": true,
|
||||
"videoUploadOnPasses": false,
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 2
|
||||
}
|
||||
},
|
||||
"integrationFolder": ".",
|
||||
"testFiles": ["cypress/integration/*.js", "**/ui_test_*.js"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,11 +30,6 @@ export default {
|
|||
"link_doctype": "Contact",
|
||||
"link_fieldname": "user"
|
||||
},
|
||||
{
|
||||
"group": "Profile",
|
||||
"link_doctype": "Chat Profile",
|
||||
"link_fieldname": "user"
|
||||
},
|
||||
],
|
||||
modified_by: 'Administrator',
|
||||
module: 'Custom',
|
||||
|
|
|
|||
|
|
@ -33,12 +33,13 @@ context('Control Duration', () => {
|
|||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('duration');
|
||||
expect(value).to.equal(3889800);
|
||||
cy.hide_dialog();
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide days or seconds according to duration options', () => {
|
||||
get_dialog_with_duration(1, 1).as('dialog');
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=duration] input').first();
|
||||
cy.get('.duration-input[data-duration=days]').should('not.be.visible');
|
||||
cy.get('.duration-input[data-duration=seconds]').should('not.be.visible');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,19 +49,19 @@ context('Control Link', () => {
|
|||
it('should unset invalid value', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.intercept('GET', '/api/method/frappe.client.get_value*').as('get_value');
|
||||
cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=link] input')
|
||||
.type('invalid value', { delay: 100 })
|
||||
.blur();
|
||||
cy.wait('@get_value');
|
||||
cy.wait('@validate_link');
|
||||
cy.get('.frappe-control[data-fieldname=link] input').should('have.value', '');
|
||||
});
|
||||
|
||||
it('should route to form on arrow click', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
cy.intercept('GET', '/api/method/frappe.client.get_value*').as('get_value');
|
||||
cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
|
||||
|
||||
cy.get('@todos').then(todos => {
|
||||
|
|
@ -69,7 +69,7 @@ context('Control Link', () => {
|
|||
cy.get('@input').focus();
|
||||
cy.wait('@search_link');
|
||||
cy.get('@input').type(todos[0]).blur();
|
||||
cy.wait('@get_value');
|
||||
cy.wait('@validate_link');
|
||||
cy.get('@input').focus();
|
||||
cy.findByTitle('Open Link')
|
||||
.should('be.visible')
|
||||
|
|
@ -81,11 +81,11 @@ context('Control Link', () => {
|
|||
it('should fetch valid value', () => {
|
||||
cy.get('@todos').then(todos => {
|
||||
cy.visit(`/app/todo/${todos[0]}`);
|
||||
cy.intercept('GET', '/api/method/frappe.client.get_value*').as('get_value');
|
||||
cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=assigned_by] input').focus().as('input');
|
||||
cy.get('@input').type('Administrator', {delay: 100}).blur();
|
||||
cy.wait('@get_value');
|
||||
cy.wait('@validate_link');
|
||||
cy.get('.frappe-control[data-fieldname=assigned_by_full_name] .control-value').should(
|
||||
'contain', 'Administrator'
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ context('Control Rating', () => {
|
|||
fields: [{
|
||||
'fieldname': 'rate',
|
||||
'fieldtype': 'Rating',
|
||||
'options': 7
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
|
@ -19,12 +20,13 @@ context('Control Rating', () => {
|
|||
|
||||
cy.get('div.rating')
|
||||
.children('svg')
|
||||
.find('.right-half')
|
||||
.first()
|
||||
.click()
|
||||
.should('have.class', 'star-click');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
var value = dialog.get_value('rate');
|
||||
expect(value).to.equal(1);
|
||||
expect(value).to.equal(1/7);
|
||||
dialog.hide();
|
||||
});
|
||||
});
|
||||
|
|
@ -34,10 +36,21 @@ context('Control Rating', () => {
|
|||
|
||||
cy.get('div.rating')
|
||||
.children('svg')
|
||||
.find('.right-half')
|
||||
.first()
|
||||
.invoke('trigger', 'mouseenter')
|
||||
.should('have.class', 'star-hover')
|
||||
.invoke('trigger', 'mouseleave')
|
||||
.should('not.have.class', 'star-hover');
|
||||
});
|
||||
|
||||
it('check number of stars in rating', () => {
|
||||
get_dialog_with_rating();
|
||||
|
||||
cy.get('div.rating')
|
||||
.first()
|
||||
.children('svg')
|
||||
.should('have.length', 7);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
22
cypress/integration/dashboard_chart.js
Normal file
22
cypress/integration/dashboard_chart.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
context('Dashboard Chart', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('Check filter populate for child table doctype', () => {
|
||||
cy.visit('/app/dashboard-chart/new-dashboard-chart-1');
|
||||
cy.get('[data-fieldname="parent_document_type"]').should('have.css', 'display', 'none');
|
||||
|
||||
cy.get_field('document_type', 'Link');
|
||||
cy.fill_field('document_type', 'Workspace Link', 'Link').focus().blur();
|
||||
cy.get_field('document_type', 'Link').should('have.value', 'Workspace Link');
|
||||
|
||||
cy.fill_field('chart_name', 'Test Chart', 'Data');
|
||||
|
||||
cy.get('[data-fieldname="filters_json"]').click().wait(200);
|
||||
cy.get('.modal-body .filter-action-buttons .add-filter').click();
|
||||
cy.get('.modal-body .fieldname-select-area').click();
|
||||
cy.get('.modal-actions .btn-modal-close').click();
|
||||
});
|
||||
});
|
||||
|
|
@ -92,15 +92,15 @@ context('Control Date, Time and DateTime', () => {
|
|||
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'
|
||||
doc_value: '2019-12-02 00:30:12', // system timezone (America/New_York)
|
||||
input_value: '02.12.2019 11:00:12' // admin timezone (Asia/Kolkata)
|
||||
},
|
||||
{
|
||||
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'
|
||||
doc_value: '2019-12-02 00:30:00', // system timezone (America/New_York)
|
||||
input_value: '12-02-2019 11:00' // admin timezone (Asia/Kolkata)
|
||||
}
|
||||
];
|
||||
datetime_formats.forEach(d => {
|
||||
|
|
|
|||
45
cypress/integration/first_day_of_the_week.js
Normal file
45
cypress/integration/first_day_of_the_week.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
context("First Day of the Week", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/app/system-settings');
|
||||
cy.findByText('Date and Number Format').click();
|
||||
});
|
||||
|
||||
it("Date control starts with same day as selected in System Settings", () => {
|
||||
cy.intercept('POST', '/api/method/frappe.core.doctype.system_settings.system_settings.load').as("load_settings");
|
||||
cy.fill_field('first_day_of_the_week', 'Tuesday', 'Select');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.wait("@load_settings");
|
||||
cy.dialog({
|
||||
title: 'Date',
|
||||
fields: [
|
||||
{
|
||||
label: 'Date',
|
||||
fieldname: 'date',
|
||||
fieldtype: 'Date'
|
||||
}
|
||||
]
|
||||
});
|
||||
cy.get_field('date').click();
|
||||
cy.get('.datepicker--day-name').eq(0).should('have.text', 'Tu');
|
||||
});
|
||||
|
||||
it("Calendar view starts with same day as selected in System Settings", () => {
|
||||
cy.intercept('POST', '/api/method/frappe.core.doctype.system_settings.system_settings.load').as("load_settings");
|
||||
cy.fill_field('first_day_of_the_week', 'Monday', 'Select');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.wait("@load_settings");
|
||||
cy.visit("app/todo/view/calendar/default");
|
||||
cy.get('.fc-day-header > span').eq(0).should('have.text', 'Mon');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.visit('/app/system-settings');
|
||||
cy.findByText('Date and Number Format').click();
|
||||
cy.fill_field('first_day_of_the_week', 'Sunday', 'Select');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
});
|
||||
});
|
||||
|
|
@ -8,11 +8,7 @@ context('Form', () => {
|
|||
});
|
||||
it('create a new form', () => {
|
||||
cy.visit('/app/todo/new');
|
||||
cy.get('[data-fieldname="description"] .ql-editor')
|
||||
.first()
|
||||
.click()
|
||||
.type('this is a test todo');
|
||||
cy.wait(300);
|
||||
cy.get_field('description', 'Text Editor').type('this is a test todo', {force: true}).wait(200);
|
||||
cy.get('.page-title').should('contain', 'Not Saved');
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
|
|
@ -20,29 +16,34 @@ context('Form', () => {
|
|||
}).as('form_save');
|
||||
cy.get('.primary-action').click();
|
||||
cy.wait('@form_save').its('response.statusCode').should('eq', 200);
|
||||
|
||||
cy.visit('/app/todo');
|
||||
cy.wait(300);
|
||||
cy.get('.title-text').should('be.visible').and('contain', 'To Do');
|
||||
cy.get('.page-head').findByTitle('To Do').should('exist');
|
||||
cy.get('.list-row').should('contain', 'this is a test todo');
|
||||
});
|
||||
|
||||
it('navigates between documents with child table list filters applied', () => {
|
||||
cy.visit('/app/contact');
|
||||
cy.add_filter();
|
||||
cy.get('.filter-field .input-with-feedback.form-control').type('123', { force: true });
|
||||
cy.findByRole('button', {name: 'Apply Filters'}).click({ force: true });
|
||||
cy.visit('/app/contact/Test Form Contact 3');
|
||||
|
||||
cy.clear_filters();
|
||||
cy.get('.standard-filter-section [data-fieldname="name"] input').type('Test Form Contact 3').blur();
|
||||
cy.click_listview_row_item(0);
|
||||
|
||||
cy.get('.prev-doc').should('be.visible').click();
|
||||
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
|
||||
cy.hide_dialog();
|
||||
cy.get('.next-doc').click();
|
||||
cy.wait(200);
|
||||
|
||||
cy.get('.next-doc').should('be.visible').click();
|
||||
cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible');
|
||||
cy.hide_dialog();
|
||||
cy.contains('Test Form Contact 2').should('not.exist');
|
||||
cy.get('.title-text').should('contain', 'Test Form Contact 3');
|
||||
|
||||
cy.get('#page-Contact .page-head').findByTitle('Test Form Contact 3').should('exist');
|
||||
|
||||
// clear filters
|
||||
cy.visit('/app/contact');
|
||||
cy.clear_filters();
|
||||
});
|
||||
|
||||
it('validates behaviour of Data options validations in child table', () => {
|
||||
// test email validations for set_invalid controller
|
||||
let website_input = 'website.in';
|
||||
|
|
|
|||
40
cypress/integration/grid_keyboard_shortcut.js
Normal file
40
cypress/integration/grid_keyboard_shortcut.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
context('Grid Keyboard Shortcut', () => {
|
||||
let total_count = 0;
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
beforeEach(() => {
|
||||
cy.reload();
|
||||
cy.visit('/app/contact/new-contact-1');
|
||||
cy.get('.frappe-control[data-fieldname="email_ids"]').find(".grid-add-row").click();
|
||||
});
|
||||
it('Insert new row at the end', () => {
|
||||
cy.add_new_row_in_grid('{ctrl}{shift}{downarrow}', (cy, total_count) => {
|
||||
cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', `${total_count+1}`);
|
||||
}, total_count);
|
||||
});
|
||||
it('Insert new row at the top', () => {
|
||||
cy.add_new_row_in_grid('{ctrl}{shift}{uparrow}', (cy) => {
|
||||
cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', '2');
|
||||
});
|
||||
});
|
||||
it('Insert new row below', () => {
|
||||
cy.add_new_row_in_grid('{ctrl}{downarrow}', (cy) => {
|
||||
cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', '1');
|
||||
});
|
||||
});
|
||||
it('Insert new row above', () => {
|
||||
cy.add_new_row_in_grid('{ctrl}{uparrow}', (cy) => {
|
||||
cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', '2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('add_new_row_in_grid', (shortcut_keys, callbackFn, total_count) => {
|
||||
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
|
||||
cy.get('@table').find('.grid-body [data-fieldname="email_id"]').first().click();
|
||||
cy.get('@table').find('.grid-body [data-fieldname="email_id"]')
|
||||
.first().type(shortcut_keys);
|
||||
|
||||
callbackFn(cy, total_count);
|
||||
});
|
||||
|
|
@ -13,7 +13,7 @@ context('Grid Pagination', () => {
|
|||
it('creates pages for child table', () => {
|
||||
cy.visit('/app/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('.current-page-number').should('have.value', '1');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '20');
|
||||
cy.get('@table').find('.grid-body .grid-row').should('have.length', 50);
|
||||
});
|
||||
|
|
@ -21,10 +21,10 @@ context('Grid Pagination', () => {
|
|||
cy.visit('/app/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('.current-page-number').should('have.value', '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('.current-page-number').should('have.value', '1');
|
||||
cy.get('@table').find('.grid-body .grid-row').first().should('have.attr', 'data-idx', '1');
|
||||
});
|
||||
it('adds and deletes rows and changes page', () => {
|
||||
|
|
@ -32,14 +32,35 @@ context('Grid Pagination', () => {
|
|||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').findByRole('button', {name: '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('.current-page-number').should('have.value', '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').findByRole('button', {name: 'Delete'}).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('.current-page-number').should('have.value', '20');
|
||||
cy.get('@table').find('.total-page-number').should('contain', '20');
|
||||
});
|
||||
it('go to specific page, use up and down arrow, type characters, 0 page and more than existing page', () => {
|
||||
cy.visit('/app/contact/Test Contact');
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
cy.get('@table').find('.current-page-number').focus().clear().type('17').blur();
|
||||
cy.get('@table').find('.grid-body .row-index').should('contain', 801);
|
||||
|
||||
cy.get('@table').find('.current-page-number').focus().type('{uparrow}{uparrow}');
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '19');
|
||||
|
||||
cy.get('@table').find('.current-page-number').focus().type('{downarrow}{downarrow}');
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '17');
|
||||
|
||||
cy.get('@table').find('.current-page-number').focus().clear().type('700').blur();
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '20');
|
||||
|
||||
cy.get('@table').find('.current-page-number').focus().clear().type('0').blur();
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '1');
|
||||
|
||||
cy.get('@table').find('.current-page-number').focus().clear().type('abc').blur();
|
||||
cy.get('@table').find('.current-page-number').should('have.value', '1');
|
||||
});
|
||||
// it('deletes all rows', ()=> {
|
||||
// cy.visit('/app/contact/Test Contact');
|
||||
// cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
|
||||
|
|
|
|||
|
|
@ -7,19 +7,13 @@ context('List View', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('Keep checkbox checked after Bulk Update', () => {
|
||||
it('Keep checkbox checked after Refresh', () => {
|
||||
cy.go_to_list('ToDo');
|
||||
cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true });
|
||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
|
||||
cy.get('.dropdown-menu li:visible .dropdown-item .menu-item-label[data-label="Edit"]').click();
|
||||
|
||||
cy.get('.modal-body .form-control[data-fieldname="field"]').first().select('Due Date').wait(200);
|
||||
cy.fill_field('value', '09-28-21', 'Date');
|
||||
|
||||
cy.get('.modal-footer .standard-actions .btn-primary').click();
|
||||
cy.wait(500);
|
||||
|
||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click();
|
||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible');
|
||||
cy.intercept('/api/method/frappe.desk.reportview.get').as('list-refresh');
|
||||
cy.get('button[data-original-title="Refresh"]').click();
|
||||
cy.wait('@list-refresh');
|
||||
cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,32 +2,47 @@ context('MultiSelectDialog', () => {
|
|||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
const contact_template = {
|
||||
"doctype": "Contact",
|
||||
"first_name": "Test",
|
||||
"status": "Passive",
|
||||
"email_ids": [
|
||||
{
|
||||
"doctype": "Contact Email",
|
||||
"email_id": "test@example.com",
|
||||
"is_primary": 0
|
||||
}
|
||||
]
|
||||
};
|
||||
const promises = Array.from({length: 25})
|
||||
.map(() => cy.insert_doc('Contact', contact_template, true));
|
||||
Promise.all(promises);
|
||||
});
|
||||
|
||||
function open_multi_select_dialog() {
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
new frappe.ui.form.MultiSelectDialog({
|
||||
doctype: "Assignment Rule",
|
||||
doctype: "Contact",
|
||||
target: {},
|
||||
setters: {
|
||||
document_type: null,
|
||||
priority: null
|
||||
status: null,
|
||||
gender: null
|
||||
},
|
||||
add_filters_group: 1,
|
||||
allow_child_item_selection: 1,
|
||||
child_fieldname: "assignment_days",
|
||||
child_columns: ["day"]
|
||||
child_fieldname: "email_ids",
|
||||
child_columns: ["email_id", "is_primary"]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it('multi select dialog api works', () => {
|
||||
it('checks multi select dialog api works', () => {
|
||||
open_multi_select_dialog();
|
||||
cy.get_open_dialog().should('contain', 'Select Assignment Rules');
|
||||
cy.get_open_dialog().should('contain', 'Select Contacts');
|
||||
});
|
||||
|
||||
it('checks for filters', () => {
|
||||
['search_term', 'document_type', 'priority'].forEach(fieldname => {
|
||||
['search_term', 'status', 'gender'].forEach(fieldname => {
|
||||
cy.get_open_dialog().get(`.frappe-control[data-fieldname="${fieldname}"]`).should('exist');
|
||||
});
|
||||
|
||||
|
|
@ -42,17 +57,43 @@ context('MultiSelectDialog', () => {
|
|||
|
||||
cy.get_open_dialog()
|
||||
.get(`.frappe-control[data-fieldname="allow_child_item_selection"]`)
|
||||
.find('input[data-fieldname="allow_child_item_selection"]')
|
||||
.should('exist')
|
||||
.click();
|
||||
.click({force: true});
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.frappe-control[data-fieldname="child_selection_area"]`)
|
||||
.should('exist');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('contain', 'Assignment Rule');
|
||||
.get(`.dt-row-header`).should('contain', 'Contact');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('contain', 'Day');
|
||||
.get(`.dt-row-header`).should('contain', 'Email Id');
|
||||
|
||||
cy.get_open_dialog()
|
||||
.get(`.dt-row-header`).should('contain', 'Is Primary');
|
||||
});
|
||||
|
||||
it('tests more button', () => {
|
||||
cy.get_open_dialog()
|
||||
.get(`.frappe-control[data-fieldname="more_btn"]`)
|
||||
.should('exist')
|
||||
.as('more-btn');
|
||||
|
||||
cy.get_open_dialog().get('.list-item-container').should(($rows) => {
|
||||
expect($rows).to.have.length(20);
|
||||
});
|
||||
|
||||
cy.intercept('POST', 'api/method/frappe.client.get_list').as('get-more-records');
|
||||
cy.get('@more-btn').find('button').click({force: true});
|
||||
cy.wait('@get-more-records');
|
||||
|
||||
cy.get_open_dialog().get('.list-item-container').should(($rows) => {
|
||||
if ($rows.length <= 20) {
|
||||
throw new Error("More button doesn't work");
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -13,10 +13,10 @@ context('Navigation', () => {
|
|||
|
||||
it.only('Navigate to previous page after login', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.findByTitle('To Do').should('be.visible');
|
||||
cy.get('.page-head').findByTitle('To Do').should('be.visible');
|
||||
cy.request('/api/method/logout');
|
||||
cy.reload();
|
||||
cy.get('.btn-primary').contains('Login').click();
|
||||
cy.reload().as('reload');
|
||||
cy.get('@reload').get('.page-card .btn-primary').contains('Login').click();
|
||||
cy.location('pathname').should('eq', '/login');
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
|
|
|
|||
|
|
@ -2,32 +2,62 @@ context('Query Report', () => {
|
|||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
cy.insert_doc('Report', {
|
||||
'report_name': 'Test ToDo Report',
|
||||
'ref_doctype': 'ToDo',
|
||||
'report_type': 'Query Report',
|
||||
'query': 'select * from tabToDo'
|
||||
}, true).as('doc');
|
||||
cy.create_records({
|
||||
doctype: 'ToDo',
|
||||
description: 'this is a test todo for query report'
|
||||
}).as('todos');
|
||||
});
|
||||
|
||||
it('add custom column in report', () => {
|
||||
cy.visit('/app/query-report/Permitted Documents For User');
|
||||
|
||||
cy.get('.page-form.flex', { timeout: 60000 }).should('have.length', 1).then(() => {
|
||||
cy.get('#page-query-report input[data-fieldname="user"]').as('input');
|
||||
cy.get('@input').focus().type('test@erpnext.com', { delay: 100 }).blur();
|
||||
cy.get('#page-query-report input[data-fieldname="user"]').as('input-user');
|
||||
cy.get('@input-user').focus().type('test@erpnext.com', { delay: 100 }).blur();
|
||||
cy.wait(300);
|
||||
cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-test');
|
||||
cy.get('@input-test').focus().type('Role', { delay: 100 }).blur();
|
||||
cy.get('#page-query-report input[data-fieldname="doctype"]').as('input-role');
|
||||
cy.get('@input-role').focus().type('Role', { delay: 100 }).blur();
|
||||
|
||||
cy.get('.datatable').should('exist');
|
||||
cy.get('.menu-btn-group button').click({ force: true });
|
||||
cy.get('.dropdown-menu li').contains('Add Column').click({ force: true });
|
||||
cy.get('.modal-dialog').should('contain', 'Add Column');
|
||||
cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true });
|
||||
cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Add Column').click({ force: true });
|
||||
cy.get_open_dialog().get('.modal-title').should('contain', 'Add Column');
|
||||
cy.get('select[data-fieldname="doctype"]').select("Role", { force: true });
|
||||
cy.get('select[data-fieldname="field"]').select("Role Name", { force: true });
|
||||
cy.get('select[data-fieldname="insert_after"]').select("Name", { force: true });
|
||||
cy.get('button').contains('Submit').click({ force: true });
|
||||
cy.get('.menu-btn-group button').click({ force: true });
|
||||
cy.get('.dropdown-menu li').contains('Save').click({ force: true });
|
||||
cy.get('.modal-dialog').should('contain', 'Save Report');
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ force: true });
|
||||
cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true });
|
||||
cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Save').click({ timeout: 100, force: true });
|
||||
cy.get_open_dialog().get('.modal-title').should('contain', 'Save Report');
|
||||
|
||||
cy.get('input[data-fieldname="report_name"]').type("Test Report", { delay: 100, force: true });
|
||||
cy.get('button').contains('Submit').click({ timeout: 1000, force: true });
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ timeout: 1000, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
let save_report_and_open = (report, update_name) => {
|
||||
cy.get('#page-query-report .page-actions .menu-btn-group button').click({ force: true });
|
||||
cy.get('#page-query-report .menu-btn-group .dropdown-menu').contains('Save').click({ timeout: 100, force: true });
|
||||
cy.get_open_dialog().get('.modal-title').should('contain', 'Save Report');
|
||||
|
||||
cy.get('input[data-fieldname="report_name"]').type(update_name, { delay: 100, force: true });
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Submit'}).click({ timeout: 1000, force: true });
|
||||
|
||||
cy.visit('/app/query-report/'+report);
|
||||
cy.get('.datatable').should('exist');
|
||||
};
|
||||
|
||||
it('test multi level query report', () => {
|
||||
cy.visit('/app/query-report/Test ToDo Report');
|
||||
cy.get('.datatable').should('exist');
|
||||
|
||||
save_report_and_open('Test ToDo Report 1', ' 1');
|
||||
save_report_and_open('Test ToDo Report 11', '1');
|
||||
});
|
||||
});
|
||||
|
|
@ -14,48 +14,51 @@ context('Recorder', () => {
|
|||
});
|
||||
|
||||
it('Recorder Empty State', () => {
|
||||
cy.findByTitle('Recorder').should('exist');
|
||||
cy.get('.page-head').findByTitle('Recorder').should('exist');
|
||||
|
||||
cy.get('.indicator-pill').should('contain', 'Inactive').should('have.class', 'red');
|
||||
|
||||
cy.findByRole('button', {name: 'Start'}).should('exist');
|
||||
cy.findByRole('button', {name: 'Clear'}).should('exist');
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Start'}).should('exist');
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Clear'}).should('exist');
|
||||
|
||||
cy.get('.msg-box').should('contain', 'Inactive');
|
||||
cy.findByRole('button', {name: 'Start Recording'}).should('exist');
|
||||
cy.get('.msg-box').should('contain', 'Recorder is Inactive');
|
||||
cy.get('.msg-box').findByRole('button', {name: 'Start Recording'}).should('exist');
|
||||
});
|
||||
|
||||
it('Recorder Start', () => {
|
||||
cy.findByRole('button', {name: 'Start'}).click();
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Start'}).click();
|
||||
cy.get('.indicator-pill').should('contain', 'Active').should('have.class', 'green');
|
||||
|
||||
cy.get('.msg-box').should('contain', 'No Requests');
|
||||
cy.get('.msg-box').should('contain', 'No Requests found');
|
||||
|
||||
cy.visit('/app/List/DocType/List');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.wait('@list_refresh');
|
||||
|
||||
cy.get('.title-text').should('contain', 'DocType');
|
||||
cy.get('.page-head').findByTitle('DocType').should('exist');
|
||||
cy.get('.list-count').should('contain', '20 of ');
|
||||
|
||||
cy.visit('/app/recorder');
|
||||
cy.findByTitle('Recorder').should('exist');
|
||||
cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get');
|
||||
cy.get('.page-head').findByTitle('Recorder').should('exist');
|
||||
cy.get('.frappe-list .result-list').should('contain', '/api/method/frappe.desk.reportview.get');
|
||||
});
|
||||
|
||||
it('Recorder View Request', () => {
|
||||
cy.findByRole('button', {name: 'Start'}).click();
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Start'}).click();
|
||||
|
||||
cy.visit('/app/List/DocType/List');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh');
|
||||
cy.wait('@list_refresh');
|
||||
|
||||
cy.get('.title-text').should('contain', 'DocType');
|
||||
cy.get('.page-head').findByTitle('DocType').should('exist');
|
||||
cy.get('.list-count').should('contain', '20 of ');
|
||||
|
||||
cy.visit('/app/recorder');
|
||||
|
||||
cy.get('.list-row-container span').contains('/api/method/frappe').click();
|
||||
cy.get('.frappe-list .list-row-container span')
|
||||
.contains('/api/method/frappe')
|
||||
.should('be.visible')
|
||||
.click({force: true});
|
||||
|
||||
cy.url().should('include', '/recorder/request');
|
||||
cy.get('form').should('contain', '/api/method/frappe');
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ context('Report View', () => {
|
|||
cy.visit('/app/website');
|
||||
cy.insert_doc('DocType', custom_submittable_doctype, true);
|
||||
cy.clear_cache();
|
||||
});
|
||||
it('Field with enabled allow_on_submit should be editable.', () => {
|
||||
cy.insert_doc(doctype_name, {
|
||||
'title': 'Doc 1',
|
||||
'description': 'Random Text',
|
||||
|
|
@ -14,8 +16,6 @@ context('Report View', () => {
|
|||
// submit document
|
||||
'docstatus': 1
|
||||
}, true).as('doc');
|
||||
});
|
||||
it('Field with enabled allow_on_submit should be editable.', () => {
|
||||
cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
|
||||
cy.visit(`/app/List/${doctype_name}/Report`);
|
||||
// check status column added from docstatus
|
||||
|
|
@ -23,8 +23,7 @@ context('Report View', () => {
|
|||
let cell = cy.get('.dt-row-0 > .dt-cell--col-4');
|
||||
// select the cell
|
||||
cell.dblclick();
|
||||
cell.findByRole('checkbox').check({ force: true });
|
||||
cy.get('.dt-row-0 > .dt-cell--col-5').click();
|
||||
cell.get('.dt-cell__edit--col-4').findByRole('checkbox').check({ force: true });
|
||||
cy.wait('@value-update');
|
||||
cy.get('@doc').then(doc => {
|
||||
cy.call('frappe.client.get_value', {
|
||||
|
|
|
|||
|
|
@ -8,22 +8,18 @@ context('Timeline', () => {
|
|||
|
||||
it('Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo', () => {
|
||||
//Adding new ToDo
|
||||
cy.visit('/app/todo/new-todo-1');
|
||||
cy.get('[data-fieldname="description"] .ql-editor.ql-blank').type('Test ToDo', {force: true}).wait(200);
|
||||
cy.get('.page-head .page-actions').findByRole('button', {name: 'Save'}).click();
|
||||
|
||||
cy.visit('/app/todo');
|
||||
cy.click_listview_primary_button('Add ToDo');
|
||||
cy.findByRole('button', {name: 'Edit in full page'}).click();
|
||||
cy.findByTitle('New ToDo').should('be.visible');
|
||||
cy.get('[data-fieldname="description"] .ql-editor').eq(0).type('Test ToDo', {force: true});
|
||||
cy.wait(200);
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.wait(700);
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.level-item.ellipsis').eq(0).click();
|
||||
cy.click_listview_row_item(0);
|
||||
|
||||
//To check if the comment box is initially empty and tying some text into it
|
||||
cy.get('[data-fieldname="comment"] .ql-editor').should('contain', '').type('Testing Timeline');
|
||||
|
||||
//Adding new comment
|
||||
cy.findByRole('button', {name: 'Comment'}).click();
|
||||
cy.get('.comment-box').findByRole('button', {name: 'Comment'}).click();
|
||||
|
||||
//To check if the commented text is visible in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Testing Timeline');
|
||||
|
|
@ -38,21 +34,17 @@ context('Timeline', () => {
|
|||
|
||||
//Discarding comment
|
||||
cy.click_timeline_action_btn("Edit");
|
||||
cy.findByRole('button', {name: 'Dismiss'}).click();
|
||||
cy.click_timeline_action_btn("Dismiss");
|
||||
|
||||
//To check if after discarding the timeline content is same as previous
|
||||
cy.get('.timeline-content').should('contain', 'Testing Timeline 123');
|
||||
|
||||
//Deleting the added comment
|
||||
cy.get('.more-actions > .action-btn').click();
|
||||
cy.get('.more-actions .dropdown-item').contains('Delete').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
cy.get('.timeline-message-box .more-actions > .action-btn').click(); //Menu button in timeline item
|
||||
cy.get('.timeline-message-box .more-actions .dropdown-item').contains('Delete').click({ force: true });
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Yes'}).click({ force: true });
|
||||
|
||||
//Deleting the added ToDo
|
||||
cy.get('[id="page-ToDo"] .menu-btn-group [data-original-title="Menu"]').click();
|
||||
cy.get('[id="page-ToDo"] .menu-btn-group .dropdown-item').contains('Delete').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
cy.get('.timeline-content').should('not.contain', 'Testing Timeline 123');
|
||||
});
|
||||
|
||||
it('Timeline should have submit and cancel activity information', () => {
|
||||
|
|
@ -66,31 +58,32 @@ context('Timeline', () => {
|
|||
|
||||
//Adding a new entry for the created custom doctype
|
||||
cy.fill_field('title', 'Test');
|
||||
cy.findByRole('button', {name: 'Save'}).click();
|
||||
cy.findByRole('button', {name: 'Submit'}).click();
|
||||
cy.click_modal_primary_button('Save');
|
||||
cy.click_modal_primary_button('Submit');
|
||||
|
||||
cy.visit('/app/custom-submittable-doctype');
|
||||
cy.get('.list-subject > .bold > .ellipsis').eq(0).click();
|
||||
cy.click_listview_row_item(0);
|
||||
|
||||
//To check if the submission of the documemt is visible in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Administrator submitted this document');
|
||||
cy.findByRole('button', {name: 'Cancel'}).click({delay: 900});
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
cy.get('[id="page-Custom Submittable DocType"] .page-actions').findByRole('button', {name: 'Cancel'}).click();
|
||||
cy.get_open_dialog().findByRole('button', {name: 'Yes'}).click();
|
||||
|
||||
//To check if the cancellation of the documemt is visible in the timeline content
|
||||
cy.get('.timeline-content').should('contain', 'Administrator cancelled this document');
|
||||
|
||||
//Deleting the document
|
||||
cy.visit('/app/custom-submittable-doctype');
|
||||
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click();
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group > .dropdown-menu > li > .dropdown-item').contains("Delete").click();
|
||||
cy.click_modal_primary_button('Yes', {force: true, delay: 700});
|
||||
cy.select_listview_row_checkbox(0);
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
|
||||
//Deleting the custom doctype
|
||||
cy.visit('/app/doctype');
|
||||
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click();
|
||||
cy.findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.actions-btn-group [data-label="Delete"]').click();
|
||||
cy.select_listview_row_checkbox(0);
|
||||
cy.get('.page-actions').findByRole('button', {name: 'Actions'}).click();
|
||||
cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
});
|
||||
});
|
||||
29
cypress/integration/web_form.js
Normal file
29
cypress/integration/web_form.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
context('Web Form', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('Navigate and Submit a WebForm', () => {
|
||||
cy.visit('/update-profile');
|
||||
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
|
||||
cy.get('.web-form-actions .btn-primary').click();
|
||||
cy.wait(500);
|
||||
cy.get('.modal.show > .modal-dialog').should('be.visible');
|
||||
});
|
||||
|
||||
it('Navigate and Submit a MultiStep WebForm', () => {
|
||||
cy.call('frappe.tests.ui_test_helpers.update_webform_to_multistep').then(() => {
|
||||
cy.visit('/update-profile-duplicate');
|
||||
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
|
||||
cy.get('.btn-next').should('be.visible');
|
||||
cy.get('.web-form-footer .btn-primary').should('not.be.visible');
|
||||
cy.get('.btn-next').click();
|
||||
cy.get('.btn-previous').should('be.visible');
|
||||
cy.get('.btn-next').should('not.be.visible');
|
||||
cy.get('.web-form-footer .btn-primary').should('be.visible');
|
||||
cy.get('.web-form-actions .btn-primary').click();
|
||||
cy.wait(500);
|
||||
cy.get('.modal.show > .modal-dialog').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -30,7 +30,7 @@ Cypress.Commands.add('login', (email, password) => {
|
|||
email = 'Administrator';
|
||||
}
|
||||
if (!password) {
|
||||
password = Cypress.config('adminPassword');
|
||||
password = Cypress.env('adminPassword');
|
||||
}
|
||||
cy.request({
|
||||
url: '/api/method/login',
|
||||
|
|
@ -161,7 +161,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => {
|
|||
|
||||
Cypress.Commands.add('create_records', doc => {
|
||||
return cy
|
||||
.call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc})
|
||||
.call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc: JSON.stringify(doc)})
|
||||
.then(r => r.message);
|
||||
});
|
||||
|
||||
|
|
@ -193,7 +193,8 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
|
||||
let selector = `[data-fieldname="${fieldname}"] input:visible`;
|
||||
let field_element = fieldtype === 'Select' ? 'select': 'input';
|
||||
let selector = `[data-fieldname="${fieldname}"] ${field_element}:visible`;
|
||||
|
||||
if (fieldtype === 'Text Editor') {
|
||||
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]:visible`;
|
||||
|
|
@ -341,7 +342,7 @@ Cypress.Commands.add('click_sidebar_button', (btn_name) => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('click_listview_row_item', (row_no) => {
|
||||
cy.get('.list-row > .level-left > .list-subject > .bold > .ellipsis').eq(row_no).click({force: true});
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item > .ellipsis').eq(row_no).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_filter_button', () => {
|
||||
|
|
@ -353,5 +354,9 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('click_timeline_action_btn', (btn_name) => {
|
||||
cy.get('.timeline-message-box .custom-actions > .btn').contains(btn_name).click();
|
||||
cy.get('.timeline-message-box .actions .action-btn').contains(btn_name).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('select_listview_row_checkbox', (row_no) => {
|
||||
cy.get('.frappe-list .select-like > .list-row-checkbox').eq(row_no).click();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
coverage==5.5
|
||||
Faker~=8.1.0
|
||||
pyngrok~=5.0.5
|
||||
unittest-xml-reporting~=3.0.4
|
||||
|
|
|
|||
38
esbuild/build-cleanup.js
Normal file
38
esbuild/build-cleanup.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/* eslint-disable no-console */
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const glob = require("fast-glob");
|
||||
|
||||
module.exports = {
|
||||
name: 'build_cleanup',
|
||||
setup(build) {
|
||||
build.onEnd(result => {
|
||||
if (result.errors.length) return;
|
||||
clean_dist_files(Object.keys(result.metafile.outputs));
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function clean_dist_files(new_files) {
|
||||
new_files.forEach(
|
||||
file => {
|
||||
if (file.endsWith(".map")) return;
|
||||
|
||||
const pattern = file.split(".").slice(0, -2).join(".") + "*";
|
||||
glob.sync(pattern).forEach(
|
||||
file_to_delete => {
|
||||
if (file_to_delete.startsWith(file)) return;
|
||||
|
||||
fs.unlink(path.resolve(file_to_delete), err => {
|
||||
if (!err) return;
|
||||
|
||||
console.error(
|
||||
`Error deleting ${file.split(path.sep).pop()}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -1,18 +1,20 @@
|
|||
/* eslint-disable no-console */
|
||||
let path = require("path");
|
||||
let fs = require("fs");
|
||||
let glob = require("fast-glob");
|
||||
let esbuild = require("esbuild");
|
||||
let vue = require("esbuild-vue");
|
||||
let yargs = require("yargs");
|
||||
let cliui = require("cliui")();
|
||||
let chalk = require("chalk");
|
||||
let html_plugin = require("./frappe-html");
|
||||
let rtlcss = require('rtlcss');
|
||||
let postCssPlugin = require("esbuild-plugin-postcss2").default;
|
||||
let ignore_assets = require("./ignore-assets");
|
||||
let sass_options = require("./sass_options");
|
||||
let {
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const glob = require("fast-glob");
|
||||
const esbuild = require("esbuild");
|
||||
const vue = require("esbuild-vue");
|
||||
const yargs = require("yargs");
|
||||
const cliui = require("cliui")();
|
||||
const chalk = require("chalk");
|
||||
const html_plugin = require("./frappe-html");
|
||||
const rtlcss = require('rtlcss');
|
||||
const postCssPlugin = require("esbuild-plugin-postcss2").default;
|
||||
const ignore_assets = require("./ignore-assets");
|
||||
const sass_options = require("./sass_options");
|
||||
const build_cleanup_plugin = require("./build-cleanup");
|
||||
|
||||
const {
|
||||
app_list,
|
||||
assets_path,
|
||||
apps_path,
|
||||
|
|
@ -26,7 +28,7 @@ let {
|
|||
get_redis_subscriber
|
||||
} = require("./utils");
|
||||
|
||||
let argv = yargs
|
||||
const argv = yargs
|
||||
.usage("Usage: node esbuild [options]")
|
||||
.option("apps", {
|
||||
type: "string",
|
||||
|
|
@ -98,9 +100,6 @@ if (WATCH_MODE) {
|
|||
|
||||
async function execute() {
|
||||
console.time(TOTAL_BUILD_TIME);
|
||||
if (!FILES_TO_BUILD.length) {
|
||||
await clean_dist_folders(APPS);
|
||||
}
|
||||
|
||||
let results;
|
||||
try {
|
||||
|
|
@ -231,12 +230,13 @@ function get_files_to_build(files) {
|
|||
function build_files({ files, outdir }) {
|
||||
let build_plugins = [
|
||||
html_plugin,
|
||||
build_cleanup_plugin,
|
||||
vue(),
|
||||
];
|
||||
return esbuild.build(get_build_options(files, outdir, build_plugins));
|
||||
}
|
||||
|
||||
function build_style_files({ files, outdir, rtl_style=false }) {
|
||||
function build_style_files({ files, outdir, rtl_style = false }) {
|
||||
let plugins = [];
|
||||
if (rtl_style) {
|
||||
plugins.push(rtlcss);
|
||||
|
|
@ -244,6 +244,7 @@ function build_style_files({ files, outdir, rtl_style=false }) {
|
|||
|
||||
let build_plugins = [
|
||||
ignore_assets,
|
||||
build_cleanup_plugin,
|
||||
postCssPlugin({
|
||||
plugins: plugins,
|
||||
sassOptions: sass_options
|
||||
|
|
@ -313,24 +314,6 @@ function get_watch_config() {
|
|||
return null;
|
||||
}
|
||||
|
||||
async function clean_dist_folders(apps) {
|
||||
for (let app of apps) {
|
||||
let public_path = get_public_path(app);
|
||||
let paths = [
|
||||
path.resolve(public_path, "dist", "js"),
|
||||
path.resolve(public_path, "dist", "css"),
|
||||
path.resolve(public_path, "dist", "css-rtl")
|
||||
];
|
||||
for (let target of paths) {
|
||||
if (fs.existsSync(target)) {
|
||||
// rmdir is deprecated in node 16, this will work in both node 14 and 16
|
||||
let rmdir = fs.promises.rm || fs.promises.rmdir;
|
||||
await rmdir(target, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function log_built_assets(results) {
|
||||
let outputs = {};
|
||||
for (const result of results) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,11 @@ from .exceptions import *
|
|||
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
|
||||
from .utils.lazy_loader import lazy_import
|
||||
|
||||
from frappe.query_builder import get_query_builder, patch_query_execute
|
||||
from frappe.query_builder import (
|
||||
get_query_builder,
|
||||
patch_query_execute,
|
||||
patch_query_aggregation,
|
||||
)
|
||||
|
||||
__version__ = '14.0.0-dev'
|
||||
|
||||
|
|
@ -41,7 +45,8 @@ class _dict(dict):
|
|||
"""dict like object that exposes keys as attributes"""
|
||||
def __getattr__(self, key):
|
||||
ret = self.get(key)
|
||||
if not ret and key.startswith("__"):
|
||||
# "__deepcopy__" exception added to fix frappe#14833 via DFP
|
||||
if not ret and key.startswith("__") and key != "__deepcopy__":
|
||||
raise AttributeError()
|
||||
return ret
|
||||
def __setattr__(self, key, value):
|
||||
|
|
@ -210,6 +215,7 @@ def init(site, sites_path=None, new_site=False):
|
|||
|
||||
setup_module_map()
|
||||
patch_query_execute()
|
||||
patch_query_aggregation()
|
||||
|
||||
local.initialised = True
|
||||
|
||||
|
|
@ -734,17 +740,26 @@ def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=Fals
|
|||
:param doc: [optional] Checks User permissions for given doc.
|
||||
:param user: [optional] Check for given user. Default: current user.
|
||||
:param parent_doctype: Required when checking permission for a child DocType (unless doc is specified)."""
|
||||
import frappe.permissions
|
||||
|
||||
if not doctype and doc:
|
||||
doctype = doc.doctype
|
||||
|
||||
import frappe.permissions
|
||||
out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user,
|
||||
raise_exception=throw, parent_doctype=parent_doctype)
|
||||
|
||||
if throw and not out:
|
||||
if doc:
|
||||
frappe.throw(_("No permission for {0}").format(doc.doctype + " " + doc.name))
|
||||
else:
|
||||
frappe.throw(_("No permission for {0}").format(doctype))
|
||||
# mimics frappe.throw
|
||||
document_label = f"{doc.doctype} {doc.name}" if doc else doctype
|
||||
msgprint(
|
||||
_("No permission for {0}").format(document_label),
|
||||
raise_exception=ValidationError,
|
||||
title=None,
|
||||
indicator='red',
|
||||
is_minimizable=None,
|
||||
wide=None,
|
||||
as_list=False
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
|
@ -789,7 +804,9 @@ def has_website_permission(doc=None, ptype='read', user=None, verbose=False, doc
|
|||
def is_table(doctype):
|
||||
"""Returns True if `istable` property (indicating child Table) is set for given DocType."""
|
||||
def get_tables():
|
||||
return db.sql_list("select name from tabDocType where istable=1")
|
||||
return db.get_values(
|
||||
"DocType", filters={"istable": 1}, order_by=None, pluck=True
|
||||
)
|
||||
|
||||
tables = cache().get_value("is_table", get_tables)
|
||||
return doctype in tables
|
||||
|
|
@ -1195,7 +1212,7 @@ def read_file(path, raise_not_found=False):
|
|||
def get_attr(method_string):
|
||||
"""Get python method object from its name."""
|
||||
app_name = method_string.split(".")[0]
|
||||
if not local.flags.in_install and app_name not in get_installed_apps():
|
||||
if not local.flags.in_uninstall and not local.flags.in_install and app_name not in get_installed_apps():
|
||||
throw(_("App {0} is not installed").format(app_name), AppNotInstalledError)
|
||||
|
||||
modulename = '.'.join(method_string.split('.')[:-1])
|
||||
|
|
@ -1522,8 +1539,8 @@ def format(*args, **kwargs):
|
|||
import frappe.utils.formatters
|
||||
return frappe.utils.formatters.format_value(*args, **kwargs)
|
||||
|
||||
def get_print(doctype=None, name=None, print_format=None, style=None,
|
||||
html=None, as_pdf=False, doc=None, output=None, no_letterhead=0, password=None):
|
||||
def get_print(doctype=None, name=None, print_format=None, style=None, html=None,
|
||||
as_pdf=False, doc=None, output=None, no_letterhead=0, password=None, pdf_options=None):
|
||||
"""Get Print Format for given document.
|
||||
|
||||
:param doctype: DocType of document.
|
||||
|
|
@ -1542,15 +1559,15 @@ def get_print(doctype=None, name=None, print_format=None, style=None,
|
|||
local.form_dict.doc = doc
|
||||
local.form_dict.no_letterhead = no_letterhead
|
||||
|
||||
options = None
|
||||
pdf_options = pdf_options or {}
|
||||
if password:
|
||||
options = {'password': password}
|
||||
pdf_options['password'] = password
|
||||
|
||||
if not html:
|
||||
html = get_response_content("printview")
|
||||
|
||||
if as_pdf:
|
||||
return get_pdf(html, output = output, options = options)
|
||||
return get_pdf(html, options=pdf_options, output=output)
|
||||
else:
|
||||
return html
|
||||
|
||||
|
|
@ -1797,7 +1814,7 @@ def get_version(doctype, name, limit=None, head=False, raise_err=True):
|
|||
'limit': limit
|
||||
}, as_list=1)
|
||||
|
||||
from frappe.chat.util import squashify, dictify, safe_json_loads
|
||||
from frappe.utils import squashify, dictify, safe_json_loads
|
||||
|
||||
versions = []
|
||||
|
||||
|
|
@ -1855,7 +1872,7 @@ def mock(type, size=1, locale='en'):
|
|||
data = getattr(fake, type)()
|
||||
results.append(data)
|
||||
|
||||
from frappe.chat.util import squashify
|
||||
from frappe.utils import squashify
|
||||
return squashify(results)
|
||||
|
||||
def validate_and_sanitize_search_inputs(fn):
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ def init_request(request):
|
|||
else:
|
||||
frappe.connect(set_admin_as_user=False)
|
||||
|
||||
request.max_content_length = frappe.local.conf.get('max_file_size') or 10 * 1024 * 1024
|
||||
|
||||
make_form_dict(request)
|
||||
|
||||
if request.method != "OPTIONS":
|
||||
|
|
@ -183,7 +185,9 @@ def make_form_dict(request):
|
|||
if 'application/json' in (request.content_type or '') and request_data:
|
||||
args = json.loads(request_data)
|
||||
else:
|
||||
args = request.form or request.args
|
||||
args = {}
|
||||
args.update(request.args or {})
|
||||
args.update(request.form or {})
|
||||
|
||||
if not isinstance(args, dict):
|
||||
frappe.throw(_("Invalid request arguments"))
|
||||
|
|
|
|||
|
|
@ -128,7 +128,6 @@ class LoginManager:
|
|||
self.make_session()
|
||||
self.set_user_info()
|
||||
|
||||
@frappe.whitelist()
|
||||
def login(self):
|
||||
# clear cache
|
||||
frappe.clear_cache(user = frappe.form_dict.get('usr'))
|
||||
|
|
|
|||
|
|
@ -1,32 +1,47 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# Copyright (c) 2022, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from typing import Dict, Iterable, List
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.desk.form import assign_to
|
||||
import frappe.cache_manager
|
||||
from frappe import _
|
||||
from frappe.cache_manager import clear_doctype_map, get_doctype_map
|
||||
from frappe.desk.form import assign_to
|
||||
from frappe.model import log_types
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AssignmentRule(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_document_types()
|
||||
self.validate_assignment_days()
|
||||
|
||||
def clear_cache(self):
|
||||
super().clear_cache()
|
||||
clear_doctype_map(self.doctype, self.document_type)
|
||||
clear_doctype_map(self.doctype, f"due_date_rules_for_{self.document_type}")
|
||||
|
||||
def validate_document_types(self):
|
||||
if self.document_type == "ToDo":
|
||||
frappe.throw(
|
||||
_('Assignment Rule is not allowed on {0} document type').format(
|
||||
frappe.bold("ToDo")
|
||||
)
|
||||
)
|
||||
|
||||
def validate_assignment_days(self):
|
||||
assignment_days = self.get_assignment_days()
|
||||
if not len(set(assignment_days)) == len(assignment_days):
|
||||
|
||||
if len(set(assignment_days)) != len(assignment_days):
|
||||
repeated_days = get_repeated(assignment_days)
|
||||
frappe.throw(_("Assignment Day {0} has been repeated.").format(frappe.bold(repeated_days)))
|
||||
if self.document_type == 'ToDo':
|
||||
frappe.throw(_('Assignment Rule is not allowed on {0} document type').format(frappe.bold("ToDo")))
|
||||
plural = "s" if len(repeated_days) > 1 else ""
|
||||
|
||||
def on_update(self):
|
||||
clear_assignment_rule_cache(self)
|
||||
|
||||
def after_rename(self, old, new, merge):
|
||||
clear_assignment_rule_cache(self)
|
||||
|
||||
def on_trash(self):
|
||||
clear_assignment_rule_cache(self)
|
||||
frappe.throw(
|
||||
_("Assignment Day{0} {1} has been repeated.").format(
|
||||
plural,
|
||||
frappe.bold(", ".join(repeated_days))
|
||||
)
|
||||
)
|
||||
|
||||
def apply_unassign(self, doc, assignments):
|
||||
if (self.unassign_condition and
|
||||
|
|
@ -35,7 +50,6 @@ class AssignmentRule(Document):
|
|||
|
||||
return False
|
||||
|
||||
|
||||
def apply_assign(self, doc):
|
||||
if self.safe_eval('assign_condition', doc):
|
||||
return self.do_assignment(doc)
|
||||
|
|
@ -109,7 +123,7 @@ class AssignmentRule(Document):
|
|||
user = d.user,
|
||||
count = frappe.db.count('ToDo', dict(
|
||||
reference_type = self.document_type,
|
||||
owner = d.user,
|
||||
allocated_to = d.user,
|
||||
status = "Open"))
|
||||
))
|
||||
|
||||
|
|
@ -141,65 +155,68 @@ class AssignmentRule(Document):
|
|||
def is_rule_not_applicable_today(self):
|
||||
today = frappe.flags.assignment_day or frappe.utils.get_weekday()
|
||||
assignment_days = self.get_assignment_days()
|
||||
if assignment_days and not today in assignment_days:
|
||||
return True
|
||||
return assignment_days and today not in assignment_days
|
||||
|
||||
return False
|
||||
|
||||
def get_assignments(doc):
|
||||
def get_assignments(doc) -> List[Dict]:
|
||||
return frappe.get_all('ToDo', fields = ['name', 'assignment_rule'], filters = dict(
|
||||
reference_type = doc.get('doctype'),
|
||||
reference_name = doc.get('name'),
|
||||
status = ('!=', 'Cancelled')
|
||||
), limit = 5)
|
||||
), limit=5)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def bulk_apply(doctype, docnames):
|
||||
import json
|
||||
docnames = json.loads(docnames)
|
||||
|
||||
docnames = frappe.parse_json(docnames)
|
||||
background = len(docnames) > 5
|
||||
|
||||
for name in docnames:
|
||||
if background:
|
||||
frappe.enqueue('frappe.automation.doctype.assignment_rule.assignment_rule.apply', doc=None, doctype=doctype, name=name)
|
||||
else:
|
||||
apply(None, doctype=doctype, name=name)
|
||||
apply(doctype=doctype, name=name)
|
||||
|
||||
|
||||
def reopen_closed_assignment(doc):
|
||||
todo_list = frappe.db.get_all('ToDo', filters = dict(
|
||||
reference_type = doc.doctype,
|
||||
reference_name = doc.name,
|
||||
status = 'Closed'
|
||||
))
|
||||
if not todo_list:
|
||||
return False
|
||||
todo_list = frappe.get_all("ToDo", filters={
|
||||
"reference_type": doc.doctype,
|
||||
"reference_name": doc.name,
|
||||
"status": "Closed",
|
||||
}, pluck="name")
|
||||
|
||||
for todo in todo_list:
|
||||
todo_doc = frappe.get_doc('ToDo', todo.name)
|
||||
todo_doc = frappe.get_doc('ToDo', todo)
|
||||
todo_doc.status = 'Open'
|
||||
todo_doc.save(ignore_permissions=True)
|
||||
return True
|
||||
|
||||
def apply(doc, method=None, doctype=None, name=None):
|
||||
if not doctype:
|
||||
doctype = doc.doctype
|
||||
return bool(todo_list)
|
||||
|
||||
if (frappe.flags.in_patch
|
||||
|
||||
def apply(doc=None, method=None, doctype=None, name=None):
|
||||
doctype = doctype or doc.doctype
|
||||
|
||||
skip_assignment_rules = (
|
||||
frappe.flags.in_patch
|
||||
or frappe.flags.in_install
|
||||
or frappe.flags.in_setup_wizard
|
||||
or doctype in log_types):
|
||||
or doctype in log_types
|
||||
)
|
||||
|
||||
if skip_assignment_rules:
|
||||
return
|
||||
|
||||
if not doc and doctype and name:
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
|
||||
assignment_rules = frappe.cache_manager.get_doctype_map('Assignment Rule', doc.doctype, dict(
|
||||
document_type = doc.doctype, disabled = 0), order_by = 'priority desc')
|
||||
|
||||
assignment_rule_docs = []
|
||||
assignment_rules = get_doctype_map("Assignment Rule", doc.doctype, filters={
|
||||
"document_type": doc.doctype, "disabled": 0
|
||||
}, order_by="priority desc")
|
||||
|
||||
# multiple auto assigns
|
||||
for d in assignment_rules:
|
||||
assignment_rule_docs.append(frappe.get_cached_doc('Assignment Rule', d.get('name')))
|
||||
assignment_rule_docs: List[AssignmentRule] = [
|
||||
frappe.get_cached_doc("Assignment Rule", d.get('name')) for d in assignment_rules
|
||||
]
|
||||
|
||||
if not assignment_rule_docs:
|
||||
return
|
||||
|
|
@ -235,6 +252,7 @@ def apply(doc, method=None, doctype=None, name=None):
|
|||
|
||||
# apply close rule only if assignments exists
|
||||
assignments = get_assignments(doc)
|
||||
|
||||
if assignments:
|
||||
for assignment_rule in assignment_rule_docs:
|
||||
if assignment_rule.is_rule_not_applicable_today():
|
||||
|
|
@ -242,38 +260,74 @@ def apply(doc, method=None, doctype=None, name=None):
|
|||
|
||||
if not new_apply:
|
||||
# only reopen if close condition is not satisfied
|
||||
if not assignment_rule.safe_eval('close_condition', doc):
|
||||
reopen = reopen_closed_assignment(doc)
|
||||
if reopen:
|
||||
to_close_todos = assignment_rule.safe_eval('close_condition', doc)
|
||||
|
||||
if to_close_todos:
|
||||
# close todo status
|
||||
todos_to_close = frappe.get_all("ToDo", filters={
|
||||
"reference_type": doc.doctype,
|
||||
"reference_name": doc.name,
|
||||
}, pluck="name")
|
||||
|
||||
for todo in todos_to_close:
|
||||
_todo = frappe.get_doc("ToDo", todo)
|
||||
_todo.status = "Closed"
|
||||
_todo.save()
|
||||
break
|
||||
|
||||
else:
|
||||
reopened = reopen_closed_assignment(doc)
|
||||
if reopened:
|
||||
break
|
||||
|
||||
# print(f"Rule:{assignment_rule}\nDoc: {doc}\nReOpened: {reopened}")
|
||||
|
||||
assignment_rule.close_assignments(doc)
|
||||
|
||||
|
||||
def update_due_date(doc, state=None):
|
||||
# called from hook
|
||||
if (frappe.flags.in_patch
|
||||
or frappe.flags.in_install
|
||||
or frappe.flags.in_migrate
|
||||
"""Run on_update on every Document (via hooks.py)
|
||||
"""
|
||||
skip_document_update = (
|
||||
frappe.flags.in_migrate
|
||||
or frappe.flags.in_patch
|
||||
or frappe.flags.in_import
|
||||
or frappe.flags.in_setup_wizard):
|
||||
or frappe.flags.in_setup_wizard
|
||||
or frappe.flags.in_install
|
||||
)
|
||||
|
||||
if skip_document_update:
|
||||
return
|
||||
assignment_rules = frappe.cache_manager.get_doctype_map('Assignment Rule', 'due_date_rules_for_' + doc.doctype, dict(
|
||||
document_type = doc.doctype,
|
||||
disabled = 0,
|
||||
due_date_based_on = ['is', 'set']
|
||||
))
|
||||
|
||||
assignment_rules = get_doctype_map(
|
||||
doctype="Assignment Rule",
|
||||
name=f"due_date_rules_for_{doc.doctype}",
|
||||
filters={
|
||||
"due_date_based_on": ["is", "set"],
|
||||
"document_type": doc.doctype,
|
||||
"disabled": 0,
|
||||
}
|
||||
)
|
||||
|
||||
for rule in assignment_rules:
|
||||
rule_doc = frappe.get_cached_doc('Assignment Rule', rule.get('name'))
|
||||
rule_doc = frappe.get_cached_doc("Assignment Rule", rule.get("name"))
|
||||
due_date_field = rule_doc.due_date_based_on
|
||||
if doc.meta.has_field(due_date_field) and \
|
||||
doc.has_value_changed(due_date_field) and rule.get('name'):
|
||||
assignment_todos = frappe.get_all('ToDo', {
|
||||
'assignment_rule': rule.get('name'),
|
||||
'status': 'Open',
|
||||
'reference_type': doc.doctype,
|
||||
'reference_name': doc.name
|
||||
})
|
||||
field_updated = (
|
||||
doc.meta.has_field(due_date_field)
|
||||
and doc.has_value_changed(due_date_field)
|
||||
and rule.get("name")
|
||||
)
|
||||
|
||||
if field_updated:
|
||||
assignment_todos = frappe.get_all("ToDo", filters={
|
||||
"assignment_rule": rule.get("name"),
|
||||
"reference_type": doc.doctype,
|
||||
"reference_name": doc.name,
|
||||
"status": "Open",
|
||||
}, pluck="name")
|
||||
|
||||
for todo in assignment_todos:
|
||||
todo_doc = frappe.get_doc('ToDo', todo.name)
|
||||
todo_doc = frappe.get_doc('ToDo', todo)
|
||||
todo_doc.date = doc.get(due_date_field)
|
||||
todo_doc.flags.updater_reference = {
|
||||
'doctype': 'Assignment Rule',
|
||||
|
|
@ -282,20 +336,19 @@ def update_due_date(doc, state=None):
|
|||
}
|
||||
todo_doc.save(ignore_permissions=True)
|
||||
|
||||
def get_assignment_rules():
|
||||
return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))]
|
||||
|
||||
def get_repeated(values):
|
||||
unique_list = []
|
||||
diff = []
|
||||
def get_assignment_rules() -> List[str]:
|
||||
return frappe.get_all("Assignment Rule", filters={"disabled": 0}, pluck="document_type")
|
||||
|
||||
|
||||
def get_repeated(values: Iterable) -> List:
|
||||
unique = set()
|
||||
repeated = set()
|
||||
|
||||
for value in values:
|
||||
if value not in unique_list:
|
||||
unique_list.append(str(value))
|
||||
if value in unique:
|
||||
repeated.add(value)
|
||||
else:
|
||||
if value not in diff:
|
||||
diff.append(str(value))
|
||||
return " ".join(diff)
|
||||
unique.add(value)
|
||||
|
||||
def clear_assignment_rule_cache(rule):
|
||||
frappe.cache_manager.clear_doctype_map('Assignment Rule', rule.document_type)
|
||||
frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type)
|
||||
return [str(x) for x in repeated]
|
||||
|
|
|
|||
|
|
@ -1,12 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
|
||||
import unittest
|
||||
from frappe.utils import random_string
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.utils import random_string
|
||||
|
||||
|
||||
class TestAutoAssign(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
frappe.db.delete("Assignment Rule")
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.rollback()
|
||||
|
||||
def setUp(self):
|
||||
make_test_records("User")
|
||||
days = [
|
||||
|
|
@ -30,7 +40,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'owner'), 'test@example.com')
|
||||
), 'allocated_to'), 'test@example.com')
|
||||
|
||||
note = make_note(dict(public=1))
|
||||
|
||||
|
|
@ -39,7 +49,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'owner'), 'test1@example.com')
|
||||
), 'allocated_to'), 'test1@example.com')
|
||||
|
||||
clear_assignments()
|
||||
|
||||
|
|
@ -51,7 +61,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'owner'), 'test2@example.com')
|
||||
), 'allocated_to'), 'test2@example.com')
|
||||
|
||||
# check loop back to first user
|
||||
note = make_note(dict(public=1))
|
||||
|
|
@ -60,7 +70,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'owner'), 'test@example.com')
|
||||
), 'allocated_to'), 'test@example.com')
|
||||
|
||||
def test_load_balancing(self):
|
||||
self.assignment_rule.rule = 'Load Balancing'
|
||||
|
|
@ -71,11 +81,11 @@ class TestAutoAssign(unittest.TestCase):
|
|||
|
||||
# check if each user has 10 assignments (?)
|
||||
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
|
||||
self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10)
|
||||
self.assertEqual(len(frappe.get_all('ToDo', dict(allocated_to = user, reference_type = 'Note'))), 10)
|
||||
|
||||
# clear 5 assignments for first user
|
||||
# can't do a limit in "delete" since postgres does not support it
|
||||
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', owner = 'test@example.com'), limit=5):
|
||||
for d in frappe.get_all('ToDo', dict(reference_type = 'Note', allocated_to = 'test@example.com'), limit=5):
|
||||
frappe.db.delete("ToDo", {"name": d.name})
|
||||
|
||||
# add 5 more assignments
|
||||
|
|
@ -84,7 +94,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
|
||||
# check if each user still has 10 assignments
|
||||
for user in ('test@example.com', 'test1@example.com', 'test2@example.com'):
|
||||
self.assertEqual(len(frappe.get_all('ToDo', dict(owner = user, reference_type = 'Note'))), 10)
|
||||
self.assertEqual(len(frappe.get_all('ToDo', dict(allocated_to = user, reference_type = 'Note'))), 10)
|
||||
|
||||
def test_based_on_field(self):
|
||||
self.assignment_rule.rule = 'Based on Field'
|
||||
|
|
@ -119,7 +129,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'owner'), None)
|
||||
), 'allocated_to'), None)
|
||||
|
||||
def test_clear_assignment(self):
|
||||
note = make_note(dict(public=1))
|
||||
|
|
@ -129,10 +139,10 @@ class TestAutoAssign(unittest.TestCase):
|
|||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
))[0]
|
||||
), limit=1)[0]
|
||||
|
||||
todo = frappe.get_doc('ToDo', todo['name'])
|
||||
self.assertEqual(todo.owner, 'test@example.com')
|
||||
self.assertEqual(todo.allocated_to, 'test@example.com')
|
||||
|
||||
# test auto unassign
|
||||
note.public = 0
|
||||
|
|
@ -151,10 +161,10 @@ class TestAutoAssign(unittest.TestCase):
|
|||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
))[0]
|
||||
), limit=1)[0]
|
||||
|
||||
todo = frappe.get_doc('ToDo', todo['name'])
|
||||
self.assertEqual(todo.owner, 'test@example.com')
|
||||
self.assertEqual(todo.allocated_to, 'test@example.com')
|
||||
|
||||
note.content="Closed"
|
||||
note.save()
|
||||
|
|
@ -164,7 +174,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
# check if todo is closed
|
||||
self.assertEqual(todo.status, 'Closed')
|
||||
# check if closed todo retained assignment
|
||||
self.assertEqual(todo.owner, 'test@example.com')
|
||||
self.assertEqual(todo.allocated_to, 'test@example.com')
|
||||
|
||||
def check_multiple_rules(self):
|
||||
note = make_note(dict(public=1, notify_on_login=1))
|
||||
|
|
@ -174,7 +184,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'owner'), 'test@example.com')
|
||||
), 'allocated_to'), 'test@example.com')
|
||||
|
||||
def check_assignment_rule_scheduling(self):
|
||||
frappe.db.delete("Assignment Rule")
|
||||
|
|
@ -192,7 +202,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'owner'), ['test@example.com', 'test1@example.com', 'test2@example.com'])
|
||||
), 'allocated_to'), ['test@example.com', 'test1@example.com', 'test2@example.com'])
|
||||
|
||||
frappe.flags.assignment_day = "Friday"
|
||||
note = make_note(dict(public=1))
|
||||
|
|
@ -201,7 +211,7 @@ class TestAutoAssign(unittest.TestCase):
|
|||
reference_type = 'Note',
|
||||
reference_name = note.name,
|
||||
status = 'Open'
|
||||
), 'owner'), ['test3@example.com'])
|
||||
), 'allocated_to'), ['test3@example.com'])
|
||||
|
||||
def test_assignment_rule_condition(self):
|
||||
frappe.db.delete("Assignment Rule")
|
||||
|
|
|
|||
|
|
@ -96,7 +96,15 @@ class AutoRepeat(Document):
|
|||
auto_repeat_days = self.get_auto_repeat_days()
|
||||
if not len(set(auto_repeat_days)) == len(auto_repeat_days):
|
||||
repeated_days = get_repeated(auto_repeat_days)
|
||||
frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days)))
|
||||
plural = "s" if len(repeated_days) > 1 else ""
|
||||
|
||||
frappe.throw(
|
||||
_("Auto Repeat Day{0} {1} has been repeated.").format(
|
||||
plural,
|
||||
frappe.bold(", ".join(repeated_days))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_auto_repeat_id(self):
|
||||
#check if document is already on auto repeat
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from frappe.model.base_document import get_controller
|
|||
from frappe.social.doctype.post.post import frequently_visited_links
|
||||
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo
|
||||
from frappe.geo.country_info import get_all
|
||||
from frappe.utils import get_time_zone
|
||||
|
||||
def get_bootinfo():
|
||||
"""build and return boot info"""
|
||||
|
|
@ -60,6 +61,7 @@ def get_bootinfo():
|
|||
bootinfo.navbar_settings = get_navbar_settings()
|
||||
bootinfo.notification_settings = get_notification_settings()
|
||||
get_country_codes(bootinfo)
|
||||
set_time_zone(bootinfo)
|
||||
|
||||
# ipinfo
|
||||
if frappe.session.data.get('ipinfo'):
|
||||
|
|
@ -222,8 +224,8 @@ def load_translations(bootinfo):
|
|||
bootinfo["__messages"] = messages
|
||||
|
||||
def get_user_info():
|
||||
user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image',
|
||||
'gender', 'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type'],
|
||||
user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image', 'gender',
|
||||
'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type', 'time_zone'],
|
||||
filters=dict(enabled=1))
|
||||
|
||||
user_info_map = {d.name: d for d in user_info}
|
||||
|
|
@ -330,3 +332,9 @@ def get_notification_settings():
|
|||
def get_country_codes(bootinfo):
|
||||
country_codes = get_all()
|
||||
bootinfo.country_codes = frappe._dict(country_codes)
|
||||
|
||||
def set_time_zone(bootinfo):
|
||||
bootinfo.time_zone = {
|
||||
"system": get_time_zone(),
|
||||
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
session = frappe.session
|
||||
|
||||
def authenticate(user, raise_err = True):
|
||||
if session.user == 'Guest':
|
||||
if not frappe.db.exists('Chat Token', user):
|
||||
if raise_err:
|
||||
frappe.throw(_("Sorry, you're not authorized."))
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
if user != session.user:
|
||||
if raise_err:
|
||||
frappe.throw(_("Sorry, you're not authorized."))
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
// Copyright (c) 2017, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Chat Message', {
|
||||
onload: function(frm) {
|
||||
if(frm.doc.type == 'File') {
|
||||
frm.set_df_property('content', 'read_only', 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
{
|
||||
"beta": 1,
|
||||
"creation": "2017-11-10 11:10:40.011099",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"room_type",
|
||||
"type",
|
||||
"user",
|
||||
"room",
|
||||
"content",
|
||||
"mentions",
|
||||
"urls"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "room_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Room Type",
|
||||
"options": "Direct\nGroup\nVisitor",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Type",
|
||||
"options": "Content\nFile"
|
||||
},
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "room",
|
||||
"fieldtype": "Link",
|
||||
"label": "Room",
|
||||
"options": "Chat Room",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Text",
|
||||
"label": "Content",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "mentions",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Mentions"
|
||||
},
|
||||
{
|
||||
"fieldname": "urls",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "URLs"
|
||||
}
|
||||
],
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Chat",
|
||||
"name": "Chat Message",
|
||||
"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,
|
||||
"search_fields": "content, user",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "content",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
# imports - standard imports
|
||||
import json
|
||||
|
||||
# imports - third-party imports
|
||||
import requests
|
||||
from bs4 import BeautifulSoup as Soup
|
||||
|
||||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe import _, _dict
|
||||
import frappe
|
||||
|
||||
# imports - frappe module imports
|
||||
from frappe.chat import authenticate
|
||||
from frappe.chat.util import (
|
||||
get_if_empty,
|
||||
check_url,
|
||||
dictify,
|
||||
get_emojis,
|
||||
safe_json_loads,
|
||||
get_user_doc,
|
||||
squashify
|
||||
)
|
||||
|
||||
session = frappe.session
|
||||
|
||||
class ChatMessage(Document):
|
||||
pass
|
||||
|
||||
def get_message_urls(content):
|
||||
soup = Soup(content, 'html.parser')
|
||||
anchors = soup.find_all('a')
|
||||
urls = [ ]
|
||||
|
||||
for anchor in anchors:
|
||||
text = anchor.text
|
||||
|
||||
if check_url(text):
|
||||
urls.append(text)
|
||||
|
||||
return urls
|
||||
|
||||
def get_message_mentions(content):
|
||||
mentions = [ ]
|
||||
tokens = content.split(' ')
|
||||
|
||||
for token in tokens:
|
||||
if token.startswith('@'):
|
||||
what = token[1:]
|
||||
if frappe.db.exists('User', what):
|
||||
mentions.append(what)
|
||||
else:
|
||||
if frappe.db.exists('User', token):
|
||||
mentions.append(token)
|
||||
|
||||
return mentions
|
||||
|
||||
def get_message_meta(content):
|
||||
'''
|
||||
Assumes content to be HTML. Sanitizes the content
|
||||
into a dict of metadata values.
|
||||
'''
|
||||
meta = _dict(
|
||||
links = [ ],
|
||||
mentions = [ ]
|
||||
)
|
||||
|
||||
meta.content = content
|
||||
meta.urls = get_message_urls(content)
|
||||
meta.mentions = get_message_mentions(content)
|
||||
|
||||
return meta
|
||||
|
||||
def sanitize_message_content(content):
|
||||
emojis = get_emojis()
|
||||
|
||||
tokens = content.split(' ')
|
||||
for token in tokens:
|
||||
if token.startswith(':') and token.endswith(':'):
|
||||
what = token[1:-1]
|
||||
|
||||
# Expensive, I know.
|
||||
for emoji in emojis:
|
||||
for alias in emoji.aliases:
|
||||
if what == alias:
|
||||
content = content.replace(token, emoji.emoji)
|
||||
|
||||
return content
|
||||
|
||||
def get_new_chat_message_doc(user, room, content, type = "Content", link = True):
|
||||
user = get_user_doc(user)
|
||||
room = frappe.get_doc('Chat Room', room)
|
||||
|
||||
meta = get_message_meta(content)
|
||||
mess = frappe.new_doc('Chat Message')
|
||||
mess.room = room.name
|
||||
mess.room_type = room.type
|
||||
mess.content = sanitize_message_content(content)
|
||||
mess.type = type
|
||||
mess.user = user.name
|
||||
|
||||
mess.mentions = json.dumps(meta.mentions)
|
||||
mess.urls = ','.join(meta.urls)
|
||||
mess.save(ignore_permissions = True)
|
||||
|
||||
if link:
|
||||
room.update(dict(
|
||||
last_message = mess.name
|
||||
))
|
||||
room.save(ignore_permissions = True)
|
||||
|
||||
return mess
|
||||
|
||||
def get_new_chat_message(user, room, content, type = "Content"):
|
||||
mess = get_new_chat_message_doc(user, room, content, type)
|
||||
|
||||
resp = dict(
|
||||
name = mess.name,
|
||||
user = mess.user,
|
||||
room = mess.room,
|
||||
room_type = mess.room_type,
|
||||
content = json.loads(mess.content) if mess.type in ["File"] else mess.content,
|
||||
urls = mess.urls,
|
||||
mentions = json.loads(mess.mentions),
|
||||
creation = mess.creation,
|
||||
seen = json.loads(mess._seen) if mess._seen else [ ],
|
||||
)
|
||||
|
||||
return resp
|
||||
|
||||
@frappe.whitelist(allow_guest = True)
|
||||
def send(user, room, content, type = "Content"):
|
||||
mess = get_new_chat_message(user, room, content, type)
|
||||
|
||||
frappe.publish_realtime('frappe.chat.message:create', mess, room = room,
|
||||
after_commit = True)
|
||||
|
||||
@frappe.whitelist(allow_guest = True)
|
||||
def seen(message, user = None):
|
||||
authenticate(user)
|
||||
|
||||
has_message = frappe.db.exists('Chat Message', message)
|
||||
|
||||
if has_message:
|
||||
mess = frappe.get_doc('Chat Message', message)
|
||||
mess.add_seen(user)
|
||||
mess.load_from_db()
|
||||
room = mess.room
|
||||
resp = dict(message = message, data = dict(seen = json.loads(mess._seen) if mess._seen else []))
|
||||
|
||||
frappe.publish_realtime('frappe.chat.message:update', resp, room = room, after_commit = True)
|
||||
|
||||
def history(room, fields = None, limit = 10, start = None, end = None):
|
||||
room = frappe.get_doc('Chat Room', room)
|
||||
mess = frappe.get_all('Chat Message',
|
||||
filters = [
|
||||
('Chat Message', 'room', '=', room.name),
|
||||
('Chat Message', 'room_type', '=', room.type)
|
||||
],
|
||||
fields = fields if fields else [
|
||||
'name', 'room_type', 'room', 'content', 'type', 'user', 'mentions', 'urls', 'creation', '_seen'
|
||||
],
|
||||
order_by = 'creation'
|
||||
)
|
||||
|
||||
if not fields or 'seen' in fields:
|
||||
for m in mess:
|
||||
m['seen'] = json.loads(m._seen) if m._seen else [ ]
|
||||
del m['_seen']
|
||||
if not fields or 'content' in fields:
|
||||
for m in mess:
|
||||
m['content'] = json.loads(m.content) if m.type in ["File"] else m.content
|
||||
|
||||
frappe.enqueue('frappe.chat.doctype.chat_message.chat_message.mark_messages_as_seen',
|
||||
message_names=[m.name for m in mess], user=frappe.session.user)
|
||||
|
||||
return mess
|
||||
|
||||
def mark_messages_as_seen(message_names, user):
|
||||
'''
|
||||
Marks chat messages as seen, updates the _seen for each message
|
||||
(should be run in background process)
|
||||
'''
|
||||
for name in message_names:
|
||||
seen = frappe.db.get_value('Chat Message', name, '_seen') or '[]'
|
||||
seen = json.loads(seen)
|
||||
seen.append(user)
|
||||
seen = json.dumps(seen)
|
||||
frappe.db.set_value('Chat Message', name, '_seen', seen, update_modified=False)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get(name, rooms = None, fields = None):
|
||||
rooms, fields = safe_json_loads(rooms, fields)
|
||||
|
||||
has_message = frappe.db.exists('Chat Message', name)
|
||||
|
||||
if has_message:
|
||||
dmess = frappe.get_doc('Chat Message', name)
|
||||
data = dict(
|
||||
name = dmess.name,
|
||||
user = dmess.user,
|
||||
room = dmess.room,
|
||||
room_type = dmess.room_type,
|
||||
content = json.loads(dmess.content) if dmess.type in ["File"] else dmess.content,
|
||||
type = dmess.type,
|
||||
urls = dmess.urls,
|
||||
mentions = dmess.mentions,
|
||||
creation = dmess.creation,
|
||||
seen = get_if_empty(dmess._seen, [ ])
|
||||
)
|
||||
|
||||
return data
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
frappe.listview_settings['Chat Message'] = {
|
||||
filters: [
|
||||
['Chat Message', 'user', '==', frappe.session.user, true]
|
||||
// I need an or_filter here.
|
||||
// ['Chat Room', 'owner', '==', frappe.session.user, true],
|
||||
// ['Chat Room', frappe.session.user, 'in', 'users', true]
|
||||
]
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/* eslint semi: "never" */
|
||||
frappe.ui.form.on('Chat Profile', {
|
||||
refresh: function (form) {
|
||||
if ( form.doc.name !== frappe.session.user ) {
|
||||
form.disable_save()
|
||||
form.set_read_only(true)
|
||||
// There's one more that faris@frappe.io told me to add here. form.refresh_fields()?
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
{
|
||||
"autoname": "field:user",
|
||||
"beta": 1,
|
||||
"creation": "2017-11-13 18:26:57.943027",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"user",
|
||||
"status",
|
||||
"chat_background",
|
||||
"notifications",
|
||||
"message_preview",
|
||||
"notification_tones",
|
||||
"conversation_tones",
|
||||
"settings",
|
||||
"enable_chat"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Online",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Online\nAway\nBusy\nOffline"
|
||||
},
|
||||
{
|
||||
"fieldname": "chat_background",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Chat Background"
|
||||
},
|
||||
{
|
||||
"fieldname": "notifications",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Notifications"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "message_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "Message Preview"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "notification_tones",
|
||||
"fieldtype": "Check",
|
||||
"label": "Notification Tones"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "conversation_tones",
|
||||
"fieldtype": "Check",
|
||||
"label": "Conversation Tones"
|
||||
},
|
||||
{
|
||||
"fieldname": "settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Settings"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "enable_chat",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Chat"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"modified": "2019-11-07 13:21:36.414961",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Chat",
|
||||
"name": "Chat Profile",
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
import frappe
|
||||
|
||||
# imports - frappe module imports
|
||||
from frappe.core.doctype.version.version import get_diff
|
||||
from frappe.chat.doctype.chat_room import chat_room
|
||||
from frappe.chat.util import (
|
||||
safe_json_loads,
|
||||
filter_dict,
|
||||
dictify
|
||||
)
|
||||
|
||||
session = frappe.session
|
||||
|
||||
class ChatProfile(Document):
|
||||
def on_update(self):
|
||||
if not self.is_new():
|
||||
b, a = self.get_doc_before_save(), self
|
||||
diff = dictify(get_diff(a, b))
|
||||
if diff:
|
||||
user = session.user
|
||||
|
||||
fields = [changed[0] for changed in diff.changed]
|
||||
|
||||
if 'status' in fields:
|
||||
rooms = chat_room.get(user, filters = ['Chat Room', 'type', '=', 'Direct'])
|
||||
update = dict(user = user, data = dict(status = self.status))
|
||||
|
||||
for room in rooms:
|
||||
frappe.publish_realtime('frappe.chat.profile:update', update, room = room.name, after_commit = True)
|
||||
|
||||
if 'enable_chat' in fields:
|
||||
update = dict(user = user, data = dict(enable_chat = bool(self.enable_chat)))
|
||||
frappe.publish_realtime('frappe.chat.profile:update', update, user = user, after_commit = True)
|
||||
|
||||
def authenticate(user):
|
||||
if user != session.user:
|
||||
frappe.throw(_("Sorry, you're not authorized."))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get(user, fields = None):
|
||||
duser = frappe.get_doc('User', user)
|
||||
|
||||
if frappe.db.exists('Chat Profile', user):
|
||||
dprof = frappe.get_doc('Chat Profile', user)
|
||||
|
||||
# If you're adding something here, make sure the client recieves it.
|
||||
profile = dict(
|
||||
# User
|
||||
name = duser.name,
|
||||
email = duser.email,
|
||||
first_name = duser.first_name,
|
||||
last_name = duser.last_name,
|
||||
username = duser.username,
|
||||
avatar = duser.user_image,
|
||||
bio = duser.bio,
|
||||
# Chat Profile
|
||||
status = dprof.status,
|
||||
chat_background = dprof.chat_background,
|
||||
message_preview = bool(dprof.message_preview),
|
||||
notification_tones = bool(dprof.notification_tones),
|
||||
conversation_tones = bool(dprof.conversation_tones),
|
||||
enable_chat = bool(dprof.enable_chat)
|
||||
)
|
||||
profile = filter_dict(profile, fields)
|
||||
|
||||
return dictify(profile)
|
||||
|
||||
@frappe.whitelist()
|
||||
def create(user, exists_ok = False, fields = None):
|
||||
authenticate(user)
|
||||
|
||||
exists_ok, fields = safe_json_loads(exists_ok, fields)
|
||||
|
||||
try:
|
||||
dprof = frappe.new_doc('Chat Profile')
|
||||
dprof.user = user
|
||||
dprof.save(ignore_permissions = True)
|
||||
except frappe.DuplicateEntryError:
|
||||
frappe.clear_messages()
|
||||
if not exists_ok:
|
||||
frappe.throw(_('Chat Profile for User {0} exists.').format(user))
|
||||
|
||||
profile = get(user, fields = fields)
|
||||
|
||||
return profile
|
||||
|
||||
@frappe.whitelist()
|
||||
def update(user, data):
|
||||
authenticate(user)
|
||||
|
||||
data = safe_json_loads(data)
|
||||
|
||||
dprof = frappe.get_doc('Chat Profile', user)
|
||||
dprof.update(data)
|
||||
dprof.save(ignore_permissions = True)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
frappe.listview_settings['Chat Profile'] =
|
||||
{
|
||||
get_indicator: function (doc)
|
||||
{
|
||||
const status = frappe.utils.squash(frappe.chat.profile.STATUSES.filter(
|
||||
s => s.name === doc.status
|
||||
));
|
||||
|
||||
return [__(status.name), status.color, `status,=,${status.name}`]
|
||||
}
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (c) 2017, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Chat Room', {
|
||||
refresh: function (form) {
|
||||
|
||||
}
|
||||
});
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
{
|
||||
"autoname": "CR.#####",
|
||||
"beta": 1,
|
||||
"creation": "2017-11-08 15:27:21.156667",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"type",
|
||||
"room_name",
|
||||
"avatar",
|
||||
"last_message",
|
||||
"message_count",
|
||||
"owner",
|
||||
"user_list",
|
||||
"users"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "Direct",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "Direct\nGroup\nVisitor",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type==\"Group\"",
|
||||
"fieldname": "room_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Name"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.type==\"Group\"",
|
||||
"fieldname": "avatar",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"label": "Avatar"
|
||||
},
|
||||
{
|
||||
"fieldname": "last_message",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Last Message"
|
||||
},
|
||||
{
|
||||
"fieldname": "message_count",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"label": "Message Count"
|
||||
},
|
||||
{
|
||||
"fieldname": "owner",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Owner",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "user_list",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Users"
|
||||
},
|
||||
{
|
||||
"fieldname": "users",
|
||||
"fieldtype": "Table",
|
||||
"label": "Users",
|
||||
"options": "Chat Room User"
|
||||
}
|
||||
],
|
||||
"image_field": "avatar",
|
||||
"modified": "2019-11-07 13:20:24.625329",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Chat",
|
||||
"name": "Chat Room",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "room_name",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "room_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
import frappe
|
||||
|
||||
# imports - frappe module imports
|
||||
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.util import (
|
||||
safe_json_loads,
|
||||
dictify,
|
||||
listify,
|
||||
squashify,
|
||||
get_if_empty
|
||||
)
|
||||
|
||||
session = frappe.session
|
||||
|
||||
|
||||
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)
|
||||
|
||||
return room
|
||||
|
||||
exists = len(get_room(owner, other)) == 1
|
||||
if bidirectional:
|
||||
exists = exists or len(get_room(other, owner)) == 1
|
||||
|
||||
return exists
|
||||
|
||||
|
||||
def get_chat_room_user_set(users, filter_=None):
|
||||
seen, uset = set(), list()
|
||||
|
||||
for u in users:
|
||||
if filter_(u) and u.user not in seen:
|
||||
uset.append(u)
|
||||
seen.add(u.user)
|
||||
|
||||
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)
|
||||
self.update(dict(
|
||||
users=users
|
||||
))
|
||||
|
||||
if self.type == "Direct":
|
||||
if len(self.users) != 1:
|
||||
frappe.throw(_('{0} room must have atmost one user.').format(self.type))
|
||||
|
||||
other = squashify(self.users)
|
||||
|
||||
if self.is_new():
|
||||
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:
|
||||
frappe.throw(_('Group name cannot be empty.'))
|
||||
|
||||
def on_update(self):
|
||||
if not self.is_new():
|
||||
before = self.get_doc_before_save()
|
||||
if not before: return
|
||||
|
||||
after = self
|
||||
diff = dictify(get_diff(before, after))
|
||||
if diff:
|
||||
update = {}
|
||||
for changed in diff.changed:
|
||||
field, old, new = changed
|
||||
|
||||
if field == 'last_message':
|
||||
new = chat_message.get(new)
|
||||
|
||||
update.update({field: new})
|
||||
|
||||
if diff.added or diff.removed:
|
||||
update.update(dict(users=[u.user for u in self.users]))
|
||||
|
||||
update = dict(room=self.name, data=update)
|
||||
|
||||
frappe.publish_realtime('frappe.chat.room:update', update, room=self.name,
|
||||
after_commit=True)
|
||||
|
||||
|
||||
@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.
|
||||
# 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
|
||||
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, []))
|
||||
|
||||
const = [] # constraints
|
||||
if rooms:
|
||||
const.append(['Chat Room', 'name', 'in', rooms])
|
||||
if filters:
|
||||
if isinstance(filters[0], list):
|
||||
const = const + filters
|
||||
else:
|
||||
const.append(filters)
|
||||
|
||||
default = ['name', 'type', 'room_name', 'creation', 'owner', 'avatar']
|
||||
handle = ['users', 'last_message']
|
||||
|
||||
param = [f for f in fields if f not in handle]
|
||||
|
||||
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'] = []
|
||||
|
||||
for duser in droom.users:
|
||||
rooms[i]['users'].append(duser.user)
|
||||
|
||||
if not fields or 'last_message' in fields:
|
||||
for i, r in enumerate(rooms):
|
||||
droom = frappe.get_doc('Chat Room', r.name)
|
||||
if droom.last_message:
|
||||
rooms[i]['last_message'] = chat_message.get(droom.last_message)
|
||||
else:
|
||||
rooms[i]['last_message'] = None
|
||||
|
||||
rooms = squashify(dictify(rooms))
|
||||
|
||||
return rooms
|
||||
|
||||
|
||||
@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=%s
|
||||
""", (frappe.session.user), as_dict=True))
|
||||
|
||||
if room:
|
||||
room = frappe.get_doc('Chat Room', room.name)
|
||||
create = False
|
||||
|
||||
if create:
|
||||
room = frappe.new_doc('Chat Room')
|
||||
room.type = kind
|
||||
room.owner = frappe.session.user
|
||||
room.room_name = name
|
||||
|
||||
dusers = []
|
||||
|
||||
if kind != 'Visitor':
|
||||
if users:
|
||||
users = listify(users)
|
||||
for user in users:
|
||||
duser = frappe.new_doc('Chat Room User')
|
||||
duser.user = user
|
||||
dusers.append(duser)
|
||||
|
||||
room.users = dusers
|
||||
else:
|
||||
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 []
|
||||
|
||||
for user in dsettings.chat_operators:
|
||||
if user.user not in users:
|
||||
# appending user to room.users will remove the user from chat_operators
|
||||
# this is undesirable, create a new Chat Room User instead
|
||||
chat_room_user = {"doctype": "Chat Room User", "user": user.user}
|
||||
room.append('users', chat_room_user)
|
||||
|
||||
room.save(ignore_permissions=True)
|
||||
|
||||
room = get(token=token, rooms=room.name)
|
||||
if room:
|
||||
users = [room.owner] + [u for u in room.users]
|
||||
|
||||
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):
|
||||
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)
|
||||
|
||||
return dictify(mess)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
frappe.listview_settings['Chat Room'] = {
|
||||
filters: [
|
||||
['Chat Room', 'owner', '=', frappe.session.user, true],
|
||||
['Chat Room User', 'user', '=', frappe.session.user, true]
|
||||
]
|
||||
};
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"beta": 1,
|
||||
"creation": "2017-11-08 15:24:21.029314",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"user",
|
||||
"is_admin"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "user",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "User",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_admin",
|
||||
"fieldtype": "Check",
|
||||
"label": "Admin"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"istable": 1,
|
||||
"modified": "2019-11-07 13:21:05.297337",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Chat",
|
||||
"name": "Chat Room User",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
import frappe
|
||||
|
||||
session = frappe.session
|
||||
|
||||
class ChatRoomUser(Document):
|
||||
pass
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// Copyright (c) 2018, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Chat Token', {
|
||||
refresh: function(frm) {
|
||||
|
||||
}
|
||||
});
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
{
|
||||
"autoname": "field:token",
|
||||
"beta": 1,
|
||||
"creation": "2018-03-26 18:20:13.825652",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"token",
|
||||
"ip_address",
|
||||
"country"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "token",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Token",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ip_address",
|
||||
"fieldtype": "Data",
|
||||
"label": "IP Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Data",
|
||||
"label": "Country"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"modified": "2019-11-07 13:21:24.514558",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Chat",
|
||||
"name": "Chat Token",
|
||||
"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,
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2018, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class ChatToken(Document):
|
||||
pass
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# imports - module imports
|
||||
from frappe.chat.util.util import (
|
||||
get_user_doc,
|
||||
squashify,
|
||||
safe_json_loads,
|
||||
filter_dict,
|
||||
get_if_empty,
|
||||
listify,
|
||||
dictify,
|
||||
check_url,
|
||||
create_test_user,
|
||||
get_emojis
|
||||
)
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
# imports - standard imports
|
||||
import unittest
|
||||
|
||||
# imports - module imports
|
||||
from frappe.chat.util import (
|
||||
get_user_doc,
|
||||
safe_json_loads
|
||||
)
|
||||
import frappe
|
||||
|
||||
class TestChatUtil(unittest.TestCase):
|
||||
def test_safe_json_loads(self):
|
||||
number = safe_json_loads("1")
|
||||
self.assertEqual(type(number), int)
|
||||
|
||||
number = safe_json_loads("1.0")
|
||||
self.assertEqual(type(number), float)
|
||||
|
||||
string = safe_json_loads("foobar")
|
||||
self.assertEqual(type(string), str)
|
||||
|
||||
array = safe_json_loads('[{ "foo": "bar" }]')
|
||||
self.assertEqual(type(array), list)
|
||||
|
||||
objekt = safe_json_loads('{ "foo": "bar" }')
|
||||
self.assertEqual(type(objekt), dict)
|
||||
|
||||
true, null = safe_json_loads("true", "null")
|
||||
self.assertEqual(true, True)
|
||||
self.assertEqual(null, None)
|
||||
|
||||
def test_get_user_doc(self):
|
||||
# Needs more test cases.
|
||||
user = get_user_doc()
|
||||
self.assertEqual(user.name, frappe.session.user)
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
# imports - standard imports
|
||||
import json
|
||||
from collections.abc import MutableMapping, MutableSequence, Sequence
|
||||
|
||||
# imports - third-party imports
|
||||
import requests
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
from frappe.exceptions import DuplicateEntryError
|
||||
from frappe.model.document import Document
|
||||
|
||||
session = frappe.session
|
||||
|
||||
|
||||
def get_user_doc(user = None):
|
||||
if isinstance(user, Document):
|
||||
return user
|
||||
|
||||
user = user or session.user
|
||||
user = frappe.get_doc('User', user)
|
||||
|
||||
return user
|
||||
|
||||
def squashify(what):
|
||||
if isinstance(what, Sequence) and len(what) == 1:
|
||||
return what[0]
|
||||
|
||||
return what
|
||||
|
||||
def safe_json_loads(*args):
|
||||
results = []
|
||||
|
||||
for arg in args:
|
||||
try:
|
||||
arg = json.loads(arg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
results.append(arg)
|
||||
|
||||
return squashify(results)
|
||||
|
||||
def filter_dict(what, keys, ignore = False):
|
||||
copy = dict()
|
||||
|
||||
if keys:
|
||||
for k in keys:
|
||||
if k not in what and not ignore:
|
||||
raise KeyError('{key} not in dict.'.format(key = k))
|
||||
else:
|
||||
copy.update({
|
||||
k: what[k]
|
||||
})
|
||||
else:
|
||||
copy = what.copy()
|
||||
|
||||
return copy
|
||||
|
||||
def get_if_empty(a, b):
|
||||
if not a:
|
||||
a = b
|
||||
return a
|
||||
|
||||
def listify(arg):
|
||||
if not isinstance(arg, list):
|
||||
arg = [arg]
|
||||
return arg
|
||||
|
||||
def dictify(arg):
|
||||
if isinstance(arg, MutableSequence):
|
||||
for i, a in enumerate(arg):
|
||||
arg[i] = dictify(a)
|
||||
elif isinstance(arg, MutableMapping):
|
||||
arg = frappe._dict(arg)
|
||||
|
||||
return arg
|
||||
|
||||
def check_url(what, raise_err = False):
|
||||
if not urlparse(what).scheme:
|
||||
if raise_err:
|
||||
raise ValueError('{what} not a valid URL.')
|
||||
else:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_test_user(module):
|
||||
try:
|
||||
test_user = frappe.new_doc('User')
|
||||
test_user.first_name = '{module}'.format(module = module)
|
||||
test_user.email = 'testuser.{module}@example.com'.format(module = module)
|
||||
test_user.save()
|
||||
except DuplicateEntryError:
|
||||
frappe.log('Test User Chat Profile exists.')
|
||||
|
||||
def get_emojis():
|
||||
redis = frappe.cache()
|
||||
emojis = redis.hget('frappe_emojis', 'emojis')
|
||||
|
||||
if not emojis:
|
||||
resp = requests.get('http://git.io/frappe-emoji')
|
||||
if resp.ok:
|
||||
emojis = resp.json()
|
||||
redis.hset('frappe_emojis', 'emojis', emojis)
|
||||
|
||||
return dictify(emojis)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
|
||||
import frappe
|
||||
from frappe.chat.util import filter_dict, safe_json_loads
|
||||
|
||||
from frappe.sessions import get_geo_ip_country
|
||||
|
||||
@frappe.whitelist(allow_guest = True)
|
||||
def settings(fields = None):
|
||||
fields = safe_json_loads(fields)
|
||||
|
||||
dsettings = frappe.get_single('Website Settings')
|
||||
response = dict(
|
||||
socketio = dict(
|
||||
port = frappe.conf.socketio_port
|
||||
),
|
||||
enable = bool(dsettings.chat_enable),
|
||||
enable_from = dsettings.chat_enable_from,
|
||||
enable_to = dsettings.chat_enable_to,
|
||||
room_name = dsettings.chat_room_name,
|
||||
welcome_message = dsettings.chat_welcome_message,
|
||||
operators = [
|
||||
duser.user for duser in dsettings.chat_operators
|
||||
]
|
||||
)
|
||||
|
||||
if fields:
|
||||
response = filter_dict(response, fields)
|
||||
|
||||
return response
|
||||
|
||||
@frappe.whitelist(allow_guest = True)
|
||||
def token():
|
||||
dtoken = frappe.new_doc('Chat Token')
|
||||
|
||||
dtoken.token = frappe.generate_hash()
|
||||
dtoken.ip_address = frappe.local.request_ip
|
||||
country = get_geo_ip_country(dtoken.ip_address)
|
||||
if country:
|
||||
dtoken.country = country['iso_code']
|
||||
dtoken.save(ignore_permissions = True)
|
||||
|
||||
return dtoken.token
|
||||
|
|
@ -18,7 +18,7 @@ Requests via FrappeClient are also handled here.
|
|||
|
||||
@frappe.whitelist()
|
||||
def get_list(doctype, fields=None, filters=None, order_by=None,
|
||||
limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True):
|
||||
limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True, or_filters=None):
|
||||
'''Returns a list of records by filters, fields, ordering and limit
|
||||
|
||||
:param doctype: DocType of the data to be queried
|
||||
|
|
@ -32,8 +32,10 @@ def get_list(doctype, fields=None, filters=None, order_by=None,
|
|||
|
||||
args = frappe._dict(
|
||||
doctype=doctype,
|
||||
parent_doctype=parent,
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
or_filters=or_filters,
|
||||
order_by=order_by,
|
||||
limit_start=limit_start,
|
||||
limit_page_length=limit_page_length,
|
||||
|
|
@ -87,7 +89,7 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
|
|||
filters = {"name": filters}
|
||||
|
||||
try:
|
||||
fields = json.loads(fieldname)
|
||||
fields = frappe.parse_json(fieldname)
|
||||
except (TypeError, ValueError):
|
||||
# name passed, not json
|
||||
fields = [fieldname]
|
||||
|
|
@ -405,3 +407,45 @@ def is_document_amended(doctype, docname):
|
|||
pass
|
||||
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_link(doctype: str, docname: str, fields=None):
|
||||
if not isinstance(doctype, str):
|
||||
frappe.throw(_("DocType must be a string"))
|
||||
|
||||
if not isinstance(docname, str):
|
||||
frappe.throw(_("Document Name must be a string"))
|
||||
|
||||
if doctype != "DocType" and not (
|
||||
frappe.has_permission(doctype, "select")
|
||||
or frappe.has_permission(doctype, "read")
|
||||
):
|
||||
frappe.throw(
|
||||
_("You do not have Read or Select Permissions for {}")
|
||||
.format(frappe.bold(doctype)),
|
||||
frappe.PermissionError
|
||||
)
|
||||
|
||||
values = frappe._dict()
|
||||
values.name = frappe.db.get_value(doctype, docname, cache=True)
|
||||
|
||||
fields = frappe.parse_json(fields)
|
||||
if not values.name or not fields:
|
||||
return values
|
||||
|
||||
try:
|
||||
values.update(get_value(doctype, fields, docname))
|
||||
except frappe.PermissionError:
|
||||
frappe.clear_last_message()
|
||||
frappe.msgprint(
|
||||
_("You need {0} permission to fetch values from {1} {2}")
|
||||
.format(
|
||||
frappe.bold(_("Read")),
|
||||
frappe.bold(doctype),
|
||||
frappe.bold(docname)
|
||||
),
|
||||
title=_("Cannot Fetch Values"),
|
||||
indicator="orange"
|
||||
)
|
||||
|
||||
return values
|
||||
|
|
|
|||
|
|
@ -55,8 +55,11 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
|
|||
@click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file')
|
||||
@click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file')
|
||||
@click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended')
|
||||
@click.option('--encryption-key', help='Backup encryption key')
|
||||
@pass_context
|
||||
def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
|
||||
def restore(context, sql_file_path, encryption_key=None, mariadb_root_username=None, mariadb_root_password=None,
|
||||
db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None,
|
||||
with_private_files=None):
|
||||
"Restore site database from an sql file"
|
||||
from frappe.installer import (
|
||||
_new_site,
|
||||
|
|
@ -66,26 +69,74 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
is_partial,
|
||||
validate_database_sql
|
||||
)
|
||||
from frappe.utils.backups import Backup
|
||||
if not os.path.exists(sql_file_path):
|
||||
print("Invalid path", sql_file_path)
|
||||
sys.exit(1)
|
||||
|
||||
_backup = Backup(sql_file_path)
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
|
||||
force = context.force or force
|
||||
decompressed_file_name = extract_sql_from_archive(sql_file_path)
|
||||
|
||||
# check if partial backup
|
||||
if is_partial(decompressed_file_name):
|
||||
click.secho(
|
||||
"Partial Backup file detected. You cannot use a partial file to restore a Frappe Site.",
|
||||
fg="red"
|
||||
)
|
||||
click.secho(
|
||||
"Use `bench partial-restore` to restore a partial backup to an existing site.",
|
||||
fg="yellow"
|
||||
)
|
||||
sys.exit(1)
|
||||
try:
|
||||
decompressed_file_name = extract_sql_from_archive(sql_file_path)
|
||||
if is_partial(decompressed_file_name):
|
||||
click.secho(
|
||||
"Partial Backup file detected. You cannot use a partial file to restore a Frappe site.",
|
||||
fg="red"
|
||||
)
|
||||
click.secho(
|
||||
"Use `bench partial-restore` to restore a partial backup to an existing site.",
|
||||
fg="yellow"
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
except UnicodeDecodeError:
|
||||
_backup.decryption_rollback()
|
||||
if encryption_key:
|
||||
click.secho(
|
||||
"Encrypted backup file detected. Decrypting using provided key.",
|
||||
fg="yellow"
|
||||
)
|
||||
_backup.backup_decryption(encryption_key)
|
||||
|
||||
else:
|
||||
click.secho(
|
||||
"Encrypted backup file detected. Decrypting using site config.",
|
||||
fg="yellow"
|
||||
)
|
||||
encryption_key = frappe.get_site_config().encryption_key
|
||||
_backup.backup_decryption(encryption_key)
|
||||
|
||||
# Rollback on unsuccessful decryrption
|
||||
if not os.path.exists(sql_file_path):
|
||||
click.secho(
|
||||
"Decryption failed. Please provide a valid key and try again.",
|
||||
fg="red"
|
||||
)
|
||||
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
decompressed_file_name = extract_sql_from_archive(sql_file_path)
|
||||
|
||||
if is_partial(decompressed_file_name):
|
||||
click.secho(
|
||||
"Partial Backup file detected. You cannot use a partial file to restore a Frappe site.",
|
||||
fg="red"
|
||||
)
|
||||
click.secho(
|
||||
"Use `bench partial-restore` to restore a partial backup to an existing site.",
|
||||
fg="yellow"
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
# check if valid SQL file
|
||||
validate_database_sql(decompressed_file_name, _raise=not force)
|
||||
|
||||
# dont allow downgrading to older versions of frappe without force
|
||||
|
|
@ -96,23 +147,51 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
)
|
||||
click.confirm(warn_message, abort=True)
|
||||
|
||||
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
|
||||
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
|
||||
verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
|
||||
force=True, db_type=frappe.conf.db_type)
|
||||
|
||||
# Extract public and/or private files to the restored site, if user has given the path
|
||||
if with_public_files:
|
||||
public = extract_files(site, with_public_files)
|
||||
os.remove(public)
|
||||
|
||||
if with_private_files:
|
||||
private = extract_files(site, with_private_files)
|
||||
os.remove(private)
|
||||
try:
|
||||
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
|
||||
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
|
||||
verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
|
||||
force=True, db_type=frappe.conf.db_type)
|
||||
|
||||
except Exception as err:
|
||||
print(err.args[1])
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
# Removing temporarily created file
|
||||
if decompressed_file_name != sql_file_path:
|
||||
os.remove(decompressed_file_name)
|
||||
_backup.decryption_rollback()
|
||||
|
||||
# Extract public and/or private files to the restored site, if user has given the path
|
||||
if with_public_files:
|
||||
# Decrypt data if there is a Key
|
||||
if encryption_key:
|
||||
_backup = Backup(with_public_files)
|
||||
_backup.backup_decryption(encryption_key)
|
||||
if not os.path.exists(with_public_files):
|
||||
_backup.decryption_rollback()
|
||||
public = extract_files(site, with_public_files)
|
||||
|
||||
# Removing temporarily created file
|
||||
os.remove(public)
|
||||
_backup.decryption_rollback()
|
||||
|
||||
|
||||
if with_private_files:
|
||||
# Decrypt data if there is a Key
|
||||
if encryption_key:
|
||||
_backup = Backup(with_private_files)
|
||||
_backup.backup_decryption(encryption_key)
|
||||
if not os.path.exists(with_private_files):
|
||||
_backup.decryption_rollback()
|
||||
private = extract_files(site, with_private_files)
|
||||
|
||||
# Removing temporarily created file
|
||||
os.remove(private)
|
||||
_backup.decryption_rollback()
|
||||
|
||||
success_message = "Site {0} has been restored{1}".format(
|
||||
site,
|
||||
|
|
@ -120,19 +199,92 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
|
|||
)
|
||||
click.secho(success_message, fg="green")
|
||||
|
||||
|
||||
@click.command('partial-restore')
|
||||
@click.argument('sql-file-path')
|
||||
@click.option("--verbose", "-v", is_flag=True)
|
||||
@click.option('--encryption-key', help='Backup encryption key')
|
||||
@pass_context
|
||||
def partial_restore(context, sql_file_path, verbose):
|
||||
from frappe.installer import partial_restore
|
||||
verbose = context.verbose or verbose
|
||||
def partial_restore(context, sql_file_path, verbose, encryption_key=None):
|
||||
from frappe.installer import partial_restore, extract_sql_from_archive
|
||||
from frappe.utils.backups import Backup
|
||||
|
||||
if not os.path.exists(sql_file_path):
|
||||
print("Invalid path", sql_file_path)
|
||||
sys.exit(1)
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
|
||||
_backup = Backup(sql_file_path)
|
||||
|
||||
verbose = context.verbose or verbose
|
||||
|
||||
frappe.connect(site=site)
|
||||
try:
|
||||
decompressed_file_name = extract_sql_from_archive(sql_file_path)
|
||||
|
||||
with open(decompressed_file_name) as f:
|
||||
header = " ".join(f.readline() for _ in range(5))
|
||||
|
||||
#Check for full backup file
|
||||
if "Partial Backup" not in header:
|
||||
click.secho(
|
||||
"Full backup file detected.Use `bench restore` to restore a Frappe Site.",
|
||||
fg="red"
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
except UnicodeDecodeError:
|
||||
_backup.decryption_rollback()
|
||||
if encryption_key:
|
||||
click.secho(
|
||||
"Encrypted backup file detected. Decrypting using provided key.",
|
||||
fg="yellow"
|
||||
)
|
||||
key = encryption_key
|
||||
|
||||
else:
|
||||
click.secho(
|
||||
"Encrypted backup file detected. Decrypting using site config.",
|
||||
fg="yellow"
|
||||
)
|
||||
key = frappe.get_site_config().encryption_key
|
||||
|
||||
_backup.backup_decryption(key)
|
||||
|
||||
# Rollback on unsuccessful decryrption
|
||||
if not os.path.exists(sql_file_path):
|
||||
click.secho(
|
||||
"Decryption failed. Please provide a valid key and try again.",
|
||||
fg="red"
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
decompressed_file_name = extract_sql_from_archive(sql_file_path)
|
||||
|
||||
with open(decompressed_file_name) as f:
|
||||
header = " ".join(f.readline() for _ in range(5))
|
||||
|
||||
#Check for Full backup file.
|
||||
if "Partial Backup" not in header:
|
||||
click.secho(
|
||||
"Full Backup file detected.Use `bench restore` to restore a Frappe Site.",
|
||||
fg="red"
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
partial_restore(sql_file_path, verbose)
|
||||
|
||||
# Removing temporarily created file
|
||||
_backup.decryption_rollback()
|
||||
if os.path.exists(sql_file_path.rstrip(".gz")):
|
||||
os.remove(sql_file_path.rstrip(".gz"))
|
||||
|
||||
frappe.destroy()
|
||||
|
||||
|
||||
|
|
@ -295,11 +447,10 @@ def disable_user(context, email):
|
|||
@pass_context
|
||||
def migrate(context, skip_failing=False, skip_search_index=False):
|
||||
"Run patches, sync schema and rebuild files/translations"
|
||||
import re
|
||||
from frappe.migrate import migrate
|
||||
|
||||
for site in context.sites:
|
||||
print('Migrating', site)
|
||||
click.secho(f"Migrating {site}", fg="green")
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
try:
|
||||
|
|
@ -309,6 +460,7 @@ def migrate(context, skip_failing=False, skip_search_index=False):
|
|||
skip_search_index=skip_search_index
|
||||
)
|
||||
finally:
|
||||
print()
|
||||
frappe.destroy()
|
||||
if not context.sites:
|
||||
raise SiteNotSpecifiedError
|
||||
|
|
@ -418,6 +570,7 @@ def backup(context, with_files=False, backup_path=None, backup_path_db=None, bac
|
|||
backup_path_private_files=None, backup_path_conf=None, ignore_backup_conf=False, verbose=False,
|
||||
compress=False, include="", exclude=""):
|
||||
"Backup"
|
||||
|
||||
from frappe.utils.backups import scheduled_backup
|
||||
verbose = verbose or context.verbose
|
||||
exit_code = 0
|
||||
|
|
@ -441,14 +594,25 @@ def backup(context, with_files=False, backup_path=None, backup_path_db=None, bac
|
|||
force=True
|
||||
)
|
||||
except Exception:
|
||||
click.secho("Backup failed for Site {0}. Database or site_config.json may be corrupted".format(site), fg="red")
|
||||
click.secho(
|
||||
"Backup failed for Site {0}. Database or site_config.json may be corrupted".format(site),
|
||||
fg="red"
|
||||
)
|
||||
if verbose:
|
||||
print(frappe.get_traceback())
|
||||
exit_code = 1
|
||||
continue
|
||||
if frappe.get_system_settings("encrypt_backup") and frappe.get_site_config().encryption_key:
|
||||
click.secho(
|
||||
"Backup encryption is turned on. Please note the backup encryption key.",
|
||||
fg="yellow"
|
||||
)
|
||||
|
||||
odb.print_summary()
|
||||
click.secho("Backup for Site {0} has been successfully completed{1}".format(site, " with files" if with_files else ""), fg="green")
|
||||
click.secho(
|
||||
"Backup for Site {0} has been successfully completed{1}".format(site, " with files" if with_files else ""),
|
||||
fg="green"
|
||||
)
|
||||
frappe.destroy()
|
||||
|
||||
if not context.sites:
|
||||
|
|
@ -456,6 +620,7 @@ def backup(context, with_files=False, backup_path=None, backup_path_db=None, bac
|
|||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
@click.command('remove-from-installed-apps')
|
||||
@click.argument('app')
|
||||
@pass_context
|
||||
|
|
@ -531,11 +696,9 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
|
|||
|
||||
drop_user_and_database(frappe.conf.db_name, root_login, root_password)
|
||||
|
||||
if not archived_sites_path:
|
||||
archived_sites_path = os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived_sites')
|
||||
archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites')
|
||||
|
||||
if not os.path.exists(archived_sites_path):
|
||||
os.mkdir(archived_sites_path)
|
||||
os.makedirs(archived_sites_path, exist_ok=True)
|
||||
|
||||
move(archived_sites_path, site)
|
||||
|
||||
|
|
@ -659,22 +822,41 @@ def publish_realtime(context, event, message, room, user, doctype, docname, afte
|
|||
|
||||
@click.command('browse')
|
||||
@click.argument('site', required=False)
|
||||
@click.option('--user', required=False, help='Login as user')
|
||||
@pass_context
|
||||
def browse(context, site):
|
||||
def browse(context, site, user=None):
|
||||
'''Opens the site on web browser'''
|
||||
import webbrowser
|
||||
site = context.sites[0] if context.sites else site
|
||||
from frappe.auth import CookieManager, LoginManager
|
||||
|
||||
site = get_site(context, raise_err=False) or site
|
||||
|
||||
if not site:
|
||||
click.echo('''Please provide site name\n\nUsage:\n\tbench browse [site-name]\nor\n\tbench --site [site-name] browse''')
|
||||
return
|
||||
raise SiteNotSpecifiedError
|
||||
|
||||
site = site.lower()
|
||||
if site not in frappe.utils.get_sites():
|
||||
click.echo(f"\nSite named {click.style(site, bold=True)} doesn't exist\n", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
if site in frappe.utils.get_sites():
|
||||
webbrowser.open(frappe.utils.get_site_url(site), new=2)
|
||||
else:
|
||||
click.echo("\nSite named \033[1m{}\033[0m doesn't exist\n".format(site))
|
||||
frappe.init(site=site)
|
||||
frappe.connect()
|
||||
|
||||
sid = ''
|
||||
if user:
|
||||
if frappe.conf.developer_mode or user == "Administrator":
|
||||
frappe.utils.set_request(path="/")
|
||||
frappe.local.cookie_manager = CookieManager()
|
||||
frappe.local.login_manager = LoginManager()
|
||||
frappe.local.login_manager.login_as(user)
|
||||
sid = f'/app?sid={frappe.session.sid}'
|
||||
else:
|
||||
click.echo("Please enable developer mode to login as a user")
|
||||
|
||||
url = f'{frappe.utils.get_site_url(site)}{sid}'
|
||||
|
||||
if user == "Administrator":
|
||||
click.echo(f'Login URL: {url}')
|
||||
|
||||
click.launch(url)
|
||||
|
||||
|
||||
@click.command('start-recording')
|
||||
|
|
|
|||
|
|
@ -723,7 +723,7 @@ def run_ui_tests(context, app, headless=False, parallel=True, with_coverage=Fals
|
|||
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile")
|
||||
|
||||
# run for headless mode
|
||||
run_or_open = 'run --browser firefox --record' if headless else 'open'
|
||||
run_or_open = 'run --browser chrome --record' if headless else 'open'
|
||||
formatted_command = f'{site_env} {password_env} {coverage_env} {cypress_path} {run_or_open}'
|
||||
|
||||
if parallel:
|
||||
|
|
@ -791,10 +791,11 @@ def request(context, args=None, path=None):
|
|||
@click.command('make-app')
|
||||
@click.argument('destination')
|
||||
@click.argument('app_name')
|
||||
def make_app(destination, app_name):
|
||||
@click.option('--no-git', is_flag=True, default=False, help='Do not initialize git repository for the app')
|
||||
def make_app(destination, app_name, no_git=False):
|
||||
"Creates a boilerplate app"
|
||||
from frappe.utils.boilerplate import make_boilerplate
|
||||
make_boilerplate(destination, app_name)
|
||||
make_boilerplate(destination, app_name, no_git=no_git)
|
||||
|
||||
|
||||
@click.command('set-config')
|
||||
|
|
|
|||
|
|
@ -11,11 +11,26 @@ class AccessLog(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_access_log(
|
||||
doctype=None,
|
||||
document=None,
|
||||
method=None,
|
||||
file_type=None,
|
||||
report_name=None,
|
||||
filters=None,
|
||||
page=None,
|
||||
columns=None,
|
||||
):
|
||||
_make_access_log(
|
||||
doctype, document, method, file_type, report_name, filters, page, columns,
|
||||
)
|
||||
|
||||
|
||||
@frappe.write_only()
|
||||
@retry(
|
||||
stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError)
|
||||
)
|
||||
def make_access_log(
|
||||
def _make_access_log(
|
||||
doctype=None,
|
||||
document=None,
|
||||
method=None,
|
||||
|
|
@ -42,6 +57,7 @@ def make_access_log(
|
|||
}).db_insert()
|
||||
|
||||
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview`
|
||||
# dont commit in test mode
|
||||
# dont commit in test mode. It must be tempting to put this block along with the in_request in the
|
||||
# whitelisted method...yeah, don't do it. That part would be executed possibly on a read only DB conn
|
||||
if not frappe.flags.in_test or in_request:
|
||||
frappe.db.commit()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,15 @@ import frappe, json
|
|||
import unittest
|
||||
|
||||
class TestComment(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
frappe.form_dict.comment = None
|
||||
frappe.form_dict.comment_email = None
|
||||
frappe.form_dict.comment_by = None
|
||||
frappe.form_dict.reference_doctype = None
|
||||
frappe.form_dict.reference_name = None
|
||||
frappe.form_dict.route = None
|
||||
frappe.local.request_ip = None
|
||||
|
||||
def test_comment_creation(self):
|
||||
test_doc = frappe.get_doc(dict(doctype = 'ToDo', description = 'test'))
|
||||
test_doc.insert()
|
||||
|
|
@ -33,8 +42,16 @@ class TestComment(unittest.TestCase):
|
|||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
|
||||
|
||||
from frappe.templates.includes.comments.comments import add_comment
|
||||
add_comment('Good comment with 10 chars', 'test@test.com', 'Good Tester',
|
||||
'Blog Post', test_blog.name, test_blog.route)
|
||||
|
||||
frappe.form_dict.comment = 'Good comment with 10 chars'
|
||||
frappe.form_dict.comment_email = 'test@test.com'
|
||||
frappe.form_dict.comment_by = 'Good Tester'
|
||||
frappe.form_dict.reference_doctype = 'Blog Post'
|
||||
frappe.form_dict.reference_name = test_blog.name
|
||||
frappe.form_dict.route = test_blog.route
|
||||
frappe.local.request_ip = '127.0.0.1'
|
||||
|
||||
add_comment()
|
||||
|
||||
self.assertEqual(frappe.get_all('Comment', fields = ['*'], filters = dict(
|
||||
reference_doctype = test_blog.doctype,
|
||||
|
|
@ -43,8 +60,10 @@ class TestComment(unittest.TestCase):
|
|||
|
||||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
|
||||
|
||||
add_comment('pleez vizits my site http://mysite.com', 'test@test.com', 'bad commentor',
|
||||
'Blog Post', test_blog.name, test_blog.route)
|
||||
frappe.form_dict.comment = 'pleez vizits my site http://mysite.com'
|
||||
frappe.form_dict.comment_by = 'bad commentor'
|
||||
|
||||
add_comment()
|
||||
|
||||
self.assertEqual(len(frappe.get_all('Comment', fields = ['*'], filters = dict(
|
||||
reference_doctype = test_blog.doctype,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
"email_inbox",
|
||||
"message_id",
|
||||
"uid",
|
||||
"imap_folder",
|
||||
"email_status",
|
||||
"has_attachment",
|
||||
"feedback_section",
|
||||
|
|
@ -382,12 +383,19 @@
|
|||
"label": "Timeline Links",
|
||||
"options": "Communication Link",
|
||||
"permlevel": 2
|
||||
},
|
||||
{
|
||||
"fieldname": "imap_folder",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "IMAP Folder",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-comment",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-25 09:44:28.963538",
|
||||
"modified": "2021-11-30 09:03:25.728637",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Communication",
|
||||
|
|
|
|||
|
|
@ -488,10 +488,12 @@ def update_parent_document_on_communication(doc):
|
|||
def update_first_response_time(parent, communication):
|
||||
if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"):
|
||||
if is_system_user(communication.sender):
|
||||
first_responded_on = communication.creation
|
||||
if parent.meta.has_field("first_responded_on") and communication.sent_or_received == "Sent":
|
||||
parent.db_set("first_responded_on", first_responded_on)
|
||||
parent.db_set("first_response_time", round(time_diff_in_seconds(first_responded_on, parent.creation), 2))
|
||||
if communication.sent_or_received == "Sent":
|
||||
first_responded_on = communication.creation
|
||||
if parent.meta.has_field("first_responded_on"):
|
||||
parent.db_set("first_responded_on", first_responded_on)
|
||||
first_response_time = round(time_diff_in_seconds(first_responded_on, parent.creation), 2)
|
||||
parent.db_set("first_response_time", first_response_time)
|
||||
|
||||
def set_avg_response_time(parent, communication):
|
||||
if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent":
|
||||
|
|
|
|||
|
|
@ -146,25 +146,43 @@ def add_attachments(name, attachments):
|
|||
})
|
||||
_file.save(ignore_permissions=True)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def mark_email_as_seen(name=None):
|
||||
@frappe.whitelist(allow_guest=True, methods=("GET",))
|
||||
def mark_email_as_seen(name: str = None):
|
||||
try:
|
||||
if name and frappe.db.exists("Communication", name) and not frappe.db.get_value("Communication", name, "read_by_recipient"):
|
||||
frappe.db.set_value("Communication", name, "read_by_recipient", 1)
|
||||
frappe.db.set_value("Communication", name, "delivery_status", "Read")
|
||||
frappe.db.set_value("Communication", name, "read_by_recipient_on", get_datetime())
|
||||
frappe.db.commit()
|
||||
update_communication_as_read(name)
|
||||
frappe.db.commit() # nosemgrep: this will be called in a GET request
|
||||
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
finally:
|
||||
# Return image as response under all circumstances
|
||||
from PIL import Image
|
||||
import io
|
||||
im = Image.new('RGBA', (1, 1))
|
||||
im.putdata([(255,255,255,0)])
|
||||
buffered_obj = io.BytesIO()
|
||||
im.save(buffered_obj, format="PNG")
|
||||
|
||||
frappe.response["type"] = 'binary'
|
||||
frappe.response["filename"] = "imaginary_pixel.png"
|
||||
frappe.response["filecontent"] = buffered_obj.getvalue()
|
||||
finally:
|
||||
frappe.response.update({
|
||||
"type": "binary",
|
||||
"filename": "imaginary_pixel.png",
|
||||
"filecontent": (
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00"
|
||||
b"\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\r"
|
||||
b"IDATx\x9cc\xf8\xff\xff?\x03\x00\x08\xfc\x02\xfe\xa7\x9a\xa0"
|
||||
b"\xa0\x00\x00\x00\x00IEND\xaeB`\x82"
|
||||
)
|
||||
})
|
||||
|
||||
def update_communication_as_read(name):
|
||||
if not name or not isinstance(name, str):
|
||||
return
|
||||
|
||||
communication = frappe.db.get_value(
|
||||
"Communication",
|
||||
name,
|
||||
"read_by_recipient",
|
||||
as_dict=True
|
||||
)
|
||||
|
||||
if not communication or communication.read_by_recipient:
|
||||
return
|
||||
|
||||
frappe.db.set_value("Communication", name, {
|
||||
"read_by_recipient": 1,
|
||||
"delivery_status": "Read",
|
||||
"read_by_recipient_on": get_datetime()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ def create_email_account():
|
|||
"unreplied_for_mins": 20,
|
||||
"send_notification_to": "test_comm@example.com",
|
||||
"pop3_server": "pop.test.example.com",
|
||||
"imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}],
|
||||
"no_remaining":"0",
|
||||
"enable_automatic_linking": 1
|
||||
}).insert(ignore_permissions=True)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import typing
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model import (
|
||||
display_fieldtypes,
|
||||
no_value_fields,
|
||||
|
|
@ -191,7 +192,7 @@ class Exporter:
|
|||
[format_column_name(df) for df in self.fields if df.parent == child_table_doctype]
|
||||
)
|
||||
)
|
||||
data = frappe.db.get_list(
|
||||
data = frappe.db.get_all(
|
||||
child_table_doctype,
|
||||
filters={
|
||||
"parent": ("in", parent_names),
|
||||
|
|
@ -215,9 +216,9 @@ class Exporter:
|
|||
for df in self.fields:
|
||||
is_parent = not df.is_child_table_field
|
||||
if is_parent:
|
||||
label = df.label
|
||||
label = _(df.label)
|
||||
else:
|
||||
label = "{0} ({1})".format(df.label, df.child_table_df.label)
|
||||
label = "{0} ({1})".format(_(df.label), _(df.child_table_df.label))
|
||||
|
||||
if label in header:
|
||||
# this label is already in the header,
|
||||
|
|
@ -227,6 +228,7 @@ class Exporter:
|
|||
label = "{0}".format(df.fieldname)
|
||||
else:
|
||||
label = "{0}.{1}".format(df.child_table_df.fieldname, df.fieldname)
|
||||
|
||||
header.append(label)
|
||||
|
||||
self.csv_array.append(header)
|
||||
|
|
@ -253,10 +255,10 @@ class Exporter:
|
|||
self.build_xlsx_response()
|
||||
|
||||
def build_csv_response(self):
|
||||
build_csv_response(self.get_csv_array_for_export(), self.doctype)
|
||||
build_csv_response(self.get_csv_array_for_export(), _(self.doctype))
|
||||
|
||||
def build_xlsx_response(self):
|
||||
build_xlsx_response(self.get_csv_array_for_export(), self.doctype)
|
||||
build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype))
|
||||
|
||||
def group_children_data_by_parent(self, children_data: typing.Dict[str, list]):
|
||||
return groupby_metric(children_data, key='parent')
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ class Importer:
|
|||
new_doc = frappe.new_doc(self.doctype)
|
||||
new_doc.update(doc)
|
||||
|
||||
if (meta.autoname or "").lower() != "prompt":
|
||||
if not doc.name and (meta.autoname or "").lower() != "prompt":
|
||||
# name can only be set directly if autoname is prompt
|
||||
new_doc.set("name", None)
|
||||
|
||||
|
|
@ -262,7 +262,7 @@ class Importer:
|
|||
rows = [header_row]
|
||||
rows += [row.data for row in self.import_file.data if row.row_number in row_indexes]
|
||||
|
||||
build_csv_response(rows, self.doctype)
|
||||
build_csv_response(rows, _(self.doctype))
|
||||
|
||||
def print_import_log(self, import_log):
|
||||
failed_records = [log for log in import_log if not log.success]
|
||||
|
|
@ -1009,18 +1009,14 @@ def build_fields_dict_for_column_matching(parent_doctype):
|
|||
out = {}
|
||||
|
||||
# doctypes and fieldname if it is a child doctype
|
||||
doctypes = [[parent_doctype, None]] + [
|
||||
[df.options, df] for df in parent_meta.get_table_fields()
|
||||
doctypes = [(parent_doctype, None)] + [
|
||||
(df.options, df) for df in parent_meta.get_table_fields()
|
||||
]
|
||||
|
||||
for doctype, table_df in doctypes:
|
||||
translated_table_label = _(table_df.label) if table_df else None
|
||||
|
||||
# name field
|
||||
name_by_label = (
|
||||
"ID" if doctype == parent_doctype else "ID ({0})".format(table_df.label)
|
||||
)
|
||||
name_by_fieldname = (
|
||||
"name" if doctype == parent_doctype else "{0}.name".format(table_df.fieldname)
|
||||
)
|
||||
name_df = frappe._dict(
|
||||
{
|
||||
"fieldtype": "Data",
|
||||
|
|
@ -1031,63 +1027,90 @@ def build_fields_dict_for_column_matching(parent_doctype):
|
|||
}
|
||||
)
|
||||
|
||||
if doctype != parent_doctype:
|
||||
if doctype == parent_doctype:
|
||||
name_headers = (
|
||||
"name", # fieldname
|
||||
"ID", # label
|
||||
_("ID"), # translated label
|
||||
)
|
||||
else:
|
||||
name_headers = (
|
||||
"{0}.name".format(table_df.fieldname), # fieldname
|
||||
"ID ({0})".format(table_df.label), # label
|
||||
"{0} ({1})".format(_("ID"), translated_table_label), # translated label
|
||||
)
|
||||
|
||||
name_df.is_child_table_field = True
|
||||
name_df.child_table_df = table_df
|
||||
|
||||
out[name_by_label] = name_df
|
||||
out[name_by_fieldname] = name_df
|
||||
for header in name_headers:
|
||||
out[header] = name_df
|
||||
|
||||
# other fields
|
||||
fields = get_standard_fields(doctype) + frappe.get_meta(doctype).fields
|
||||
for df in fields:
|
||||
label = (df.label or "").strip()
|
||||
fieldtype = df.fieldtype or "Data"
|
||||
if fieldtype in no_value_fields:
|
||||
continue
|
||||
|
||||
label = (df.label or "").strip()
|
||||
translated_label = _(label)
|
||||
parent = df.parent or parent_doctype
|
||||
if fieldtype not in no_value_fields:
|
||||
if parent_doctype == doctype:
|
||||
# for parent doctypes keys will be
|
||||
# Label
|
||||
# label
|
||||
# Label (label)
|
||||
if not out.get(label):
|
||||
# if Label is already set, don't set it again
|
||||
# in case of duplicate column headers
|
||||
out[label] = df
|
||||
out[df.fieldname] = df
|
||||
label_with_fieldname = "{0} ({1})".format(label, df.fieldname)
|
||||
out[label_with_fieldname] = df
|
||||
|
||||
if parent_doctype == doctype:
|
||||
# for parent doctypes keys will be
|
||||
# Label, fieldname, Label (fieldname)
|
||||
|
||||
for header in (label, translated_label):
|
||||
# if Label is already set, don't set it again
|
||||
# in case of duplicate column headers
|
||||
if header not in out:
|
||||
out[header] = df
|
||||
|
||||
for header in (
|
||||
df.fieldname,
|
||||
f"{label} ({df.fieldname})",
|
||||
f"{translated_label} ({df.fieldname})"
|
||||
):
|
||||
out[header] = df
|
||||
|
||||
else:
|
||||
# for child doctypes keys will be
|
||||
# Label (Table Field Label)
|
||||
# table_field.fieldname
|
||||
|
||||
# create a new df object to avoid mutation problems
|
||||
if isinstance(df, dict):
|
||||
new_df = frappe._dict(df.copy())
|
||||
else:
|
||||
# in case there are multiple table fields with the same doctype
|
||||
# for child doctypes keys will be
|
||||
# Label (Table Field Label)
|
||||
# table_field.fieldname
|
||||
table_fields = parent_meta.get(
|
||||
"fields", {"fieldtype": ["in", table_fieldtypes], "options": parent}
|
||||
)
|
||||
for table_field in table_fields:
|
||||
by_label = "{0} ({1})".format(label, table_field.label)
|
||||
by_fieldname = "{0}.{1}".format(table_field.fieldname, df.fieldname)
|
||||
new_df = df.as_dict()
|
||||
|
||||
# create a new df object to avoid mutation problems
|
||||
if isinstance(df, dict):
|
||||
new_df = frappe._dict(df.copy())
|
||||
else:
|
||||
new_df = df.as_dict()
|
||||
new_df.is_child_table_field = True
|
||||
new_df.child_table_df = table_df
|
||||
|
||||
new_df.is_child_table_field = True
|
||||
new_df.child_table_df = table_field
|
||||
out[by_label] = new_df
|
||||
out[by_fieldname] = new_df
|
||||
for header in (
|
||||
# fieldname
|
||||
"{0}.{1}".format(table_df.fieldname, df.fieldname),
|
||||
# label
|
||||
"{0} ({1})".format(label, table_df.label),
|
||||
# translated label
|
||||
"{0} ({1})".format(translated_label, translated_table_label),
|
||||
):
|
||||
out[header] = new_df
|
||||
|
||||
# if autoname is based on field
|
||||
# add an entry for "ID (Autoname Field)"
|
||||
autoname_field = get_autoname_field(parent_doctype)
|
||||
if autoname_field:
|
||||
out["ID ({})".format(autoname_field.label)] = autoname_field
|
||||
# ID field should also map to the autoname field
|
||||
out["ID"] = autoname_field
|
||||
out["name"] = autoname_field
|
||||
for header in (
|
||||
"ID ({})".format(autoname_field.label), # label
|
||||
"{0} ({1})".format(_("ID"), _(autoname_field.label)), # translated label
|
||||
|
||||
# ID field should also map to the autoname field
|
||||
"ID",
|
||||
_("ID"),
|
||||
"name",
|
||||
):
|
||||
out[header] = autoname_field
|
||||
|
||||
return out
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"search_index",
|
||||
"column_break_18",
|
||||
"options",
|
||||
"show_dashboard",
|
||||
"defaults_section",
|
||||
"default",
|
||||
"column_break_6",
|
||||
|
|
@ -97,7 +98,7 @@
|
|||
"label": "Type",
|
||||
"oldfieldname": "fieldtype",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
|
||||
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
|
|
@ -526,13 +527,20 @@
|
|||
{
|
||||
"fieldname": "column_break_35",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.fieldtype===\"Tab Break\"",
|
||||
"fieldname": "show_dashboard",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Dashboard"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-26 23:39:38.341443",
|
||||
"modified": "2022-01-03 11:56:19.812863",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
@ -540,5 +548,6 @@
|
|||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,6 @@
|
|||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// MIT License. See license.txt
|
||||
|
||||
// -------------
|
||||
// Menu Display
|
||||
// -------------
|
||||
|
||||
// $(cur_frm.wrapper).on("grid-row-render", function(e, grid_row) {
|
||||
// if(grid_row.doc && grid_row.doc.fieldtype=="Section Break") {
|
||||
// $(grid_row.row).css({"font-weight": "bold"});
|
||||
// }
|
||||
// })
|
||||
|
||||
frappe.ui.form.on('DocType', {
|
||||
refresh: function(frm) {
|
||||
frm.set_query('role', 'permissions', function(doc) {
|
||||
|
|
@ -129,7 +119,7 @@ frappe.ui.form.on('DocType', {
|
|||
}
|
||||
|
||||
frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("DocField", {
|
||||
|
|
@ -153,11 +143,10 @@ frappe.ui.form.on("DocField", {
|
|||
curr_value.doctype = doctype;
|
||||
curr_value.fieldname = fieldname;
|
||||
}
|
||||
let curr_df_link_doctype = row.fieldtype == "Link" ? row.options : null;
|
||||
|
||||
let doctypes = frm.doc.fields
|
||||
.filter(df => df.fieldtype == "Link")
|
||||
.filter(df => df.options && df.options != curr_df_link_doctype)
|
||||
.filter(df => df.options && df.fieldname != row.fieldname)
|
||||
.map(df => ({
|
||||
label: `${df.options} (${df.fieldname})`,
|
||||
value: df.fieldname
|
||||
|
|
@ -217,5 +206,11 @@ frappe.ui.form.on("DocField", {
|
|||
$doctype_select.val(curr_value.doctype);
|
||||
update_fieldname_options();
|
||||
}
|
||||
},
|
||||
|
||||
fieldtype: function(frm) {
|
||||
frm.trigger("max_attachments");
|
||||
}
|
||||
});
|
||||
|
||||
extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm}));
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -75,6 +75,7 @@ class DocType(Document):
|
|||
self.make_repeatable()
|
||||
self.validate_nestedset()
|
||||
self.validate_website()
|
||||
self.ensure_minimum_max_attachment_limit()
|
||||
validate_links_table_fieldnames(self)
|
||||
|
||||
if not self.is_new():
|
||||
|
|
@ -246,6 +247,22 @@ class DocType(Document):
|
|||
# clear website cache
|
||||
clear_cache()
|
||||
|
||||
def ensure_minimum_max_attachment_limit(self):
|
||||
"""Ensure that max_attachments is *at least* bigger than number of attach fields."""
|
||||
from frappe.model import attachment_fieldtypes
|
||||
|
||||
|
||||
if not self.max_attachments:
|
||||
return
|
||||
|
||||
total_attach_fields = len([d for d in self.fields if d.fieldtype in attachment_fieldtypes])
|
||||
if total_attach_fields > self.max_attachments:
|
||||
self.max_attachments = total_attach_fields
|
||||
field_label = frappe.bold(self.meta.get_field("max_attachments").label)
|
||||
frappe.msgprint(_("Number of attachment fields are more than {}, limit updated to {}.")
|
||||
.format(field_label, total_attach_fields),
|
||||
title=_("Insufficient attachment limit"), alert=True)
|
||||
|
||||
def change_modified_of_parent(self):
|
||||
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
|
||||
if frappe.flags.in_import:
|
||||
|
|
@ -253,7 +270,7 @@ class DocType(Document):
|
|||
parent_list = frappe.db.get_all('DocField', 'parent',
|
||||
dict(fieldtype=['in', frappe.model.table_fields], options=self.name))
|
||||
for p in parent_list:
|
||||
frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent))
|
||||
frappe.db.update("DocType", p.parent, {}, for_update=False)
|
||||
|
||||
def scrub_field_names(self):
|
||||
"""Sluggify fieldnames if not set from Label."""
|
||||
|
|
@ -364,7 +381,7 @@ class DocType(Document):
|
|||
document_cls_tag = f"class {despaced_name}(Document)"
|
||||
document_import_tag = "from frappe.model.document import Document"
|
||||
website_generator_cls_tag = f"class {despaced_name}(WebsiteGenerator)"
|
||||
website_generator_import_tag = "from frappe.website.generators.website_generator import WebsiteGenerator"
|
||||
website_generator_import_tag = "from frappe.website.website_generator import WebsiteGenerator"
|
||||
|
||||
with open(controller_path) as f:
|
||||
code = f.read()
|
||||
|
|
@ -1057,6 +1074,11 @@ def validate_fields(meta):
|
|||
if getattr(docfield, 'max_height', None) and (docfield.max_height[-2:] not in ('px', 'em')):
|
||||
frappe.throw('Max for {} height must be in px, em, rem'.format(frappe.bold(docfield.fieldname)))
|
||||
|
||||
def check_no_of_ratings(docfield):
|
||||
if docfield.fieldtype == "Rating":
|
||||
if docfield.options and (int(docfield.options) > 10 or int(docfield.options) < 3):
|
||||
frappe.throw(_('Options for Rating field can range from 3 to 10'))
|
||||
|
||||
fields = meta.get("fields")
|
||||
fieldname_list = [d.fieldname for d in fields]
|
||||
|
||||
|
|
@ -1090,6 +1112,7 @@ def validate_fields(meta):
|
|||
scrub_fetch_from(d)
|
||||
validate_data_field_type(d)
|
||||
check_max_height(d)
|
||||
check_no_of_ratings(d)
|
||||
|
||||
check_fold(fields)
|
||||
check_search_fields(meta, fields)
|
||||
|
|
@ -1260,7 +1283,7 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
|
|||
roles = [p.role for p in doc.get("permissions") or []] + default_roles
|
||||
|
||||
for role in list(set(roles)):
|
||||
if not frappe.db.exists("Role", role):
|
||||
if frappe.db.table_exists("Role", cached=False) and not frappe.db.exists("Role", role):
|
||||
r = frappe.get_doc(dict(doctype= "Role", role_name=role, desk_access=1))
|
||||
r.flags.ignore_mandatory = r.flags.ignore_permissions = True
|
||||
r.insert()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
|
|||
# test_records = frappe.get_test_records('DocType')
|
||||
|
||||
class TestDocType(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_validate_name(self):
|
||||
self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert)
|
||||
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert)
|
||||
|
|
@ -42,6 +46,7 @@ class TestDocType(unittest.TestCase):
|
|||
|
||||
doc1.insert()
|
||||
self.assertRaises(frappe.UniqueValidationError, doc2.insert)
|
||||
frappe.db.rollback()
|
||||
|
||||
dt.fields[0].unique = 0
|
||||
dt.save()
|
||||
|
|
|
|||
50
frappe/core/doctype/doctype_state/doctype_state.json
Normal file
50
frappe/core/doctype/doctype_state/doctype_state.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2021-08-23 17:21:28.345841",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"color",
|
||||
"custom"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Blue",
|
||||
"fieldname": "color",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Color",
|
||||
"options": "Blue\nCyan\nGray\nGreen\nLight Blue\nOrange\nPink\nPurple\nRed\nYellow",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "custom",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Custom"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-14 14:14:55.716378",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType State",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
8
frappe/core/doctype/doctype_state/doctype_state.py
Normal file
8
frappe/core/doctype/doctype_state/doctype_state.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class DocTypeState(Document):
|
||||
pass
|
||||
|
|
@ -8,34 +8,14 @@
|
|||
"reference_doctype",
|
||||
"reference_name",
|
||||
"column_break_3",
|
||||
"rating",
|
||||
"ip_address",
|
||||
"section_break_6",
|
||||
"feedback"
|
||||
"like",
|
||||
"ip_address"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "rating",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Rating",
|
||||
"precision": "1",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "feedback",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Feedback",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Select",
|
||||
|
|
@ -57,11 +37,17 @@
|
|||
"hidden": 1,
|
||||
"label": "IP Address",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "like",
|
||||
"fieldtype": "Check",
|
||||
"label": "Like"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-23 12:45:42.045696",
|
||||
"modified": "2021-11-10 20:53:21.255593",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Feedback",
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ class TestFeedback(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
frappe.form_dict.reference_doctype = None
|
||||
frappe.form_dict.reference_name = None
|
||||
frappe.form_dict.rating = None
|
||||
frappe.form_dict.feedback = None
|
||||
frappe.form_dict.like = None
|
||||
frappe.local.request_ip = None
|
||||
|
||||
def test_feedback_creation_updation(self):
|
||||
|
|
@ -18,23 +17,22 @@ class TestFeedback(unittest.TestCase):
|
|||
|
||||
frappe.db.delete("Feedback", {"reference_doctype": "Blog Post"})
|
||||
|
||||
from frappe.templates.includes.feedback.feedback import add_feedback, update_feedback
|
||||
from frappe.templates.includes.feedback.feedback import give_feedback
|
||||
|
||||
frappe.form_dict.reference_doctype = 'Blog Post'
|
||||
frappe.form_dict.reference_name = test_blog.name
|
||||
frappe.form_dict.rating = 5
|
||||
frappe.form_dict.feedback = 'New feedback'
|
||||
frappe.form_dict.like = True
|
||||
frappe.local.request_ip = '127.0.0.1'
|
||||
|
||||
feedback = add_feedback()
|
||||
feedback = give_feedback()
|
||||
|
||||
self.assertEqual(feedback.feedback, 'New feedback')
|
||||
self.assertEqual(feedback.rating, 5)
|
||||
self.assertEqual(feedback.like, True)
|
||||
|
||||
updated_feedback = update_feedback('Blog Post', test_blog.name, 6, 'Updated feedback')
|
||||
frappe.form_dict.like = False
|
||||
|
||||
self.assertEqual(updated_feedback.feedback, 'Updated feedback')
|
||||
self.assertEqual(updated_feedback.rating, 6)
|
||||
updated_feedback = give_feedback()
|
||||
|
||||
self.assertEqual(updated_feedback.like, False)
|
||||
|
||||
frappe.db.delete("Feedback", {"reference_doctype": "Blog Post"})
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from frappe import _, conf, safe_decode
|
|||
from frappe.model.document import Document
|
||||
from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip
|
||||
from frappe.utils.image import strip_exif_data, optimize_image
|
||||
from frappe.utils.file_manager import safe_b64decode
|
||||
|
||||
class MaxFileSizeReachedError(frappe.ValidationError):
|
||||
pass
|
||||
|
|
@ -436,7 +437,7 @@ class File(Document):
|
|||
|
||||
if b"," in self.content:
|
||||
self.content = self.content.split(b",")[1]
|
||||
self.content = base64.b64decode(self.content)
|
||||
self.content = safe_b64decode(self.content)
|
||||
|
||||
if not self.is_private:
|
||||
self.is_private = 0
|
||||
|
|
@ -569,6 +570,24 @@ class File(Document):
|
|||
frappe.local.rollback_observers.append(self)
|
||||
self.save()
|
||||
|
||||
@staticmethod
|
||||
def zip_files(files):
|
||||
from six import string_types
|
||||
|
||||
zip_file = io.BytesIO()
|
||||
zf = zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED)
|
||||
for _file in files:
|
||||
if isinstance(_file, string_types):
|
||||
_file = frappe.get_doc("File", _file)
|
||||
if not isinstance(_file, File):
|
||||
continue
|
||||
if _file.is_folder:
|
||||
continue
|
||||
zf.writestr(_file.file_name, _file.get_content())
|
||||
zf.close()
|
||||
return zip_file.getvalue()
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])
|
||||
|
||||
|
|
@ -612,6 +631,16 @@ def move_file(file_list, new_parent, old_parent):
|
|||
frappe.get_doc("File", old_parent).save()
|
||||
frappe.get_doc("File", new_parent).save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def zip_files(files):
|
||||
files = frappe.parse_json(files)
|
||||
zipped_files = File.zip_files(files)
|
||||
frappe.response["filename"] = "files.zip"
|
||||
frappe.response["filecontent"] = zipped_files
|
||||
frappe.response["type"] = "download"
|
||||
|
||||
|
||||
def setup_folder_path(filename, new_parent):
|
||||
file = frappe.get_doc("File", filename)
|
||||
file.folder = new_parent
|
||||
|
|
@ -716,13 +745,11 @@ def delete_file(path):
|
|||
os.remove(path)
|
||||
|
||||
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_max_file_size():
|
||||
return cint(conf.get('max_file_size')) or 10485760
|
||||
|
||||
|
||||
|
||||
def has_permission(doc, ptype=None, user=None):
|
||||
has_access = False
|
||||
user = user or frappe.session.user
|
||||
|
|
@ -826,7 +853,7 @@ def extract_images_from_html(doc, content, is_private=False):
|
|||
content = content.encode("utf-8")
|
||||
if b"," in content:
|
||||
content = content.split(b",")[1]
|
||||
content = base64.b64decode(content)
|
||||
content = safe_b64decode(content)
|
||||
|
||||
content = optimize_image(content, mtype)
|
||||
|
||||
|
|
@ -942,20 +969,14 @@ def get_files_by_search_text(text):
|
|||
|
||||
def update_existing_file_docs(doc):
|
||||
# Update is private and file url of all file docs that point to the same file
|
||||
frappe.db.sql("""
|
||||
UPDATE `tabFile`
|
||||
SET
|
||||
file_url = %(file_url)s,
|
||||
is_private = %(is_private)s
|
||||
WHERE
|
||||
content_hash = %(content_hash)s
|
||||
and name != %(file_name)s
|
||||
""", dict(
|
||||
file_url=doc.file_url,
|
||||
is_private=doc.is_private,
|
||||
content_hash=doc.content_hash,
|
||||
file_name=doc.name
|
||||
))
|
||||
file_doctype = frappe.qb.DocType("File")
|
||||
(
|
||||
frappe.qb.update(file_doctype)
|
||||
.set(file_doctype.file_url, doc.file_url)
|
||||
.set(file_doctype.is_private, doc.is_private)
|
||||
.where(file_doctype.content_hash == doc.content_hash)
|
||||
.where(file_doctype.name != doc.name)
|
||||
).run()
|
||||
|
||||
def attach_files_to_document(doc, event):
|
||||
""" Runs on on_update hook of all documents.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ test_content2 = 'Hello World'
|
|||
def make_test_doc():
|
||||
d = frappe.new_doc('ToDo')
|
||||
d.description = 'Test'
|
||||
d.assigned_by = frappe.session.user
|
||||
d.save()
|
||||
return d.doctype, d.name
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
"custom",
|
||||
"package",
|
||||
"app_name",
|
||||
"restrict_to_domain"
|
||||
"restrict_to_domain",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -50,6 +51,12 @@
|
|||
"fieldtype": "Link",
|
||||
"label": "Package",
|
||||
"options": "Package"
|
||||
},
|
||||
{
|
||||
"fieldname": "connections_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-sitemap",
|
||||
|
|
@ -116,7 +123,7 @@
|
|||
"link_fieldname": "module"
|
||||
}
|
||||
],
|
||||
"modified": "2021-09-05 21:58:40.253909",
|
||||
"modified": "2022-01-03 13:56:52.817954",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Module Def",
|
||||
|
|
@ -154,5 +161,6 @@
|
|||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,19 +1,23 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Module Profile', {
|
||||
refresh: function(frm) {
|
||||
frappe.ui.form.on("Module Profile", {
|
||||
refresh: function (frm) {
|
||||
if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
|
||||
if (!frm.module_editor && frm.doc.__onload && frm.doc.__onload.all_modules) {
|
||||
let module_area = $('<div style="min-height: 300px">')
|
||||
.appendTo(frm.fields_dict.module_html.wrapper);
|
||||
|
||||
const module_area = $(frm.fields_dict.module_html.wrapper);
|
||||
frm.module_editor = new frappe.ModuleEditor(frm, module_area);
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.module_editor) {
|
||||
frm.module_editor.refresh();
|
||||
frm.module_editor.show();
|
||||
}
|
||||
},
|
||||
|
||||
validate: function (frm) {
|
||||
if (frm.module_editor) {
|
||||
frm.module_editor.set_modules_in_table();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,11 +34,17 @@
|
|||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-03 15:36:52.622696",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "User",
|
||||
"link_fieldname": "module_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2021-12-03 15:47:21.296443",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Module Profile",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ class NavbarSettings(Document):
|
|||
def validate_standard_navbar_items(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
|
||||
if not doc_before_save:
|
||||
return
|
||||
|
||||
before_save_items = [item for item in \
|
||||
doc_before_save.help_dropdown + doc_before_save.settings_dropdown if item.is_standard]
|
||||
|
||||
|
|
@ -32,7 +35,3 @@ def get_app_logo():
|
|||
def get_navbar_settings():
|
||||
navbar_settings = frappe.get_single('Navbar Settings')
|
||||
return navbar_settings
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) 2016, {app_publisher} and contributors
|
||||
// Copyright (c) {year}, {app_publisher} and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2013, {app_publisher} and contributors
|
||||
# License: MIT. See LICENSE
|
||||
# Copyright (c) {year}, {app_publisher} and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,14 @@ class Report(Document):
|
|||
and not frappe.flags.in_patch):
|
||||
frappe.throw(_("You are not allowed to delete Standard Report"))
|
||||
delete_custom_role('report', self.name)
|
||||
self.delete_prepared_reports()
|
||||
|
||||
def delete_prepared_reports(self):
|
||||
prepared_reports = frappe.get_all("Prepared Report", filters={'ref_report_doctype': self.name}, pluck='name')
|
||||
|
||||
for report in prepared_reports:
|
||||
frappe.delete_doc("Prepared Report", report, ignore_missing=True, force=True,
|
||||
delete_permanently=True)
|
||||
|
||||
def get_columns(self):
|
||||
return [d.as_dict(no_default_fields = True) for d in self.columns]
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ import frappe
|
|||
from ..role import desk_properties
|
||||
|
||||
def execute():
|
||||
frappe.reload_doctype('user')
|
||||
frappe.reload_doctype('role')
|
||||
for role in frappe.get_all('Role', ['name', 'desk_access']):
|
||||
role_doc = frappe.get_doc('Role', role.name)
|
||||
for key in desk_properties:
|
||||
role_doc.set(key, role_doc.desk_access)
|
||||
role_doc.save()
|
||||
role_doc.save()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
"navigation_settings_section",
|
||||
"search_bar",
|
||||
"notifications",
|
||||
"chat",
|
||||
"list_settings_section",
|
||||
"list_sidebar",
|
||||
"bulk_actions",
|
||||
|
|
@ -85,12 +84,6 @@
|
|||
"fieldtype": "Check",
|
||||
"label": "Search Bar"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "chat",
|
||||
"fieldtype": "Check",
|
||||
"label": "Chat"
|
||||
},
|
||||
{
|
||||
"fieldname": "list_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
|
|
@ -155,10 +148,11 @@
|
|||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-27 10:35:37.638350",
|
||||
"modified": "2021-10-08 14:06:55.729364",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Role",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,15 +2,22 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
desk_properties = ("search_bar", "notifications", "chat", "list_sidebar",
|
||||
desk_properties = ("search_bar", "notifications", "list_sidebar",
|
||||
"bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard")
|
||||
|
||||
STANDARD_ROLES = (
|
||||
"Administrator",
|
||||
"System Manager",
|
||||
"Script Manager",
|
||||
"All",
|
||||
"Guest"
|
||||
)
|
||||
|
||||
class Role(Document):
|
||||
def before_rename(self, old, new, merge=False):
|
||||
if old in ("Guest", "Administrator", "System Manager", "All"):
|
||||
if old in STANDARD_ROLES:
|
||||
frappe.throw(frappe._("Standard roles cannot be renamed"))
|
||||
|
||||
def after_insert(self):
|
||||
|
|
@ -23,7 +30,7 @@ class Role(Document):
|
|||
self.set_desk_properties()
|
||||
|
||||
def disable_role(self):
|
||||
if self.name in ("Guest", "Administrator", "System Manager", "All"):
|
||||
if self.name in STANDARD_ROLES:
|
||||
frappe.throw(frappe._("Standard roles cannot be disabled"))
|
||||
else:
|
||||
self.remove_roles()
|
||||
|
|
@ -82,4 +89,4 @@ def role_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
report_filters.extend(filters)
|
||||
|
||||
return frappe.get_all('Role', limit_start=start, limit_page_length=page_len,
|
||||
filters=report_filters, as_list=1)
|
||||
filters=report_filters, as_list=1)
|
||||
|
|
|
|||
|
|
@ -1,175 +1,80 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "role_profile",
|
||||
"beta": 0,
|
||||
"creation": "2017-08-31 04:16:38.764465",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"autoname": "role_profile",
|
||||
"creation": "2017-08-31 04:16:38.764465",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"role_profile",
|
||||
"roles_html",
|
||||
"roles"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "role_profile",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Role Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"fieldname": "role_profile",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Role Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "roles_html",
|
||||
"fieldtype": "HTML",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Roles HTML",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "roles_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Roles HTML",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "roles",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Roles Assigned",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Has Role",
|
||||
"permlevel": 1,
|
||||
"precision": "",
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "roles",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"label": "Roles Assigned",
|
||||
"options": "Has Role",
|
||||
"permlevel": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-10-17 11:05:11.183066",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Role Profile",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "User",
|
||||
"link_fieldname": "role_profile_name"
|
||||
}
|
||||
],
|
||||
"modified": "2021-12-03 15:45:45.270963",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Role Profile",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"permlevel": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "role_profile",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "role_profile",
|
||||
"track_changes": 1
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue