Merge branch 'develop' into phone_field_control

This commit is contained in:
Noah Jacob 2022-01-07 13:12:33 +05:30 committed by GitHub
commit e86378f26f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
453 changed files with 9726 additions and 8926 deletions

View file

@ -148,6 +148,7 @@
"context": true,
"before": true,
"beforeEach": true,
"after": true,
"qz": true,
"localforage": true,
"extend_cscript": true

View file

@ -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__":

View file

@ -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'";

View file

@ -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"}'

View file

@ -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

View file

@ -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

View file

@ -13,6 +13,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false

View file

@ -13,6 +13,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false

1
.gitignore vendored
View file

@ -11,6 +11,7 @@ dist/
frappe/docs/current
frappe/public/dist
.vscode
.vs
node_modules
.kdev4/
*.kdev4

View file

@ -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

View file

@ -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"

View file

@ -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"]
}

View file

@ -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',

View file

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

View file

@ -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'
);

View file

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

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

View file

@ -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 => {

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

View file

@ -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';

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

View file

@ -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');

View file

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

View file

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

View file

@ -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');

View file

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

View file

@ -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');

View file

@ -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', {

View file

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

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

View file

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

View file

@ -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
View 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()}`
);
});
}
);
}
);
}

View file

@ -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) {

View file

@ -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):

View file

@ -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"))

View file

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

View file

@ -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]

View file

@ -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")

View file

@ -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

View file

@ -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()
}

View file

@ -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

View file

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

View file

@ -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
}

View file

@ -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

View file

@ -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]
]
};

View file

@ -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()?
}
}
});

View file

@ -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
}

View file

@ -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)

View file

@ -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}`]
}
};

View file

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

View file

@ -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
}

View file

@ -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)

View file

@ -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]
]
};

View file

@ -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
}

View file

@ -1,8 +0,0 @@
# imports - module imports
from frappe.model.document import Document
import frappe
session = frappe.session
class ChatRoomUser(Document):
pass

View file

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

View file

@ -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
}

View file

@ -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

View file

@ -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
)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

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

View file

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

View file

@ -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()

View file

@ -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,

View file

@ -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",

View file

@ -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":

View file

@ -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()
})

View file

@ -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)

View file

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

View file

@ -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

View file

@ -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": []
}

View file

@ -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

View file

@ -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()

View file

@ -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()

View 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
}

View 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

View file

@ -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",

View file

@ -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"})

View file

@ -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.

View file

@ -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

View file

@ -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
}

View file

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

View file

@ -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": [
{

View file

@ -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

View file

@ -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 */

View file

@ -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

View file

@ -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]

View file

@ -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()

View file

@ -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": [
{

View file

@ -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)

View file

@ -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