diff --git a/.mergify.yml b/.mergify.yml index 63fe1a0086..838ce75835 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -5,6 +5,7 @@ pull_request_rules: - and: - author!=surajshetty3416 - author!=gavindsouza + - author!=deepeshgarg007 - or: - base=version-13 - base=version-12 diff --git a/cypress/integration/control_dynamic_link.js b/cypress/integration/control_dynamic_link.js new file mode 100644 index 0000000000..cc1eb0b695 --- /dev/null +++ b/cypress/integration/control_dynamic_link.js @@ -0,0 +1,128 @@ +context('Dynamic Link', () => { + before(() => { + cy.login(); + cy.visit('/app/doctype'); + return cy.window().its('frappe').then(frappe => { + return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', { + name: 'Test Dynamic Link', + fields: [ + { + "label": "Document Type", + "fieldname": "doc_type", + "fieldtype": "Link", + "options": "DocType", + "in_list_view": 1, + "in_standard_filter": 1, + }, + { + "label": "Document ID", + "fieldname": "doc_id", + "fieldtype": "Dynamic Link", + "options": "doc_type", + "in_list_view": 1, + "in_standard_filter": 1, + }, + ] + }); + }); + }); + + + function get_dialog_with_dynamic_link() { + return cy.dialog({ + title: 'Dynamic Link', + fields: [{ + "label": "Document Type", + "fieldname": "doc_type", + "fieldtype": "Link", + "options": "DocType", + "in_list_view": 1, + }, + { + "label": "Document ID", + "fieldname": "doc_id", + "fieldtype": "Dynamic Link", + "options": "doc_type", + "in_list_view": 1, + }] + }); + } + + function get_dialog_with_dynamic_link_option() { + return cy.dialog({ + title: 'Dynamic Link', + fields: [{ + "label": "Document Type", + "fieldname": "doc_type", + "fieldtype": "Link", + "options": "DocType", + "in_list_view": 1, + }, + { + "label": "Document ID", + "fieldname": "doc_id", + "fieldtype": "Dynamic Link", + "get_options": () => { + return "User"; + }, + "in_list_view": 1, + }] + }); + } + + it('Creating a dynamic link by passing option as function and verifying it in a dialog', () => { + get_dialog_with_dynamic_link_option().as('dialog'); + cy.get_field('doc_type').clear(); + cy.fill_field('doc_type', 'User', 'Link'); + cy.get_field('doc_id').click(); + + //Checking if the listbox have length greater than 0 + cy.get('[data-fieldname="doc_id"]').find('.awesomplete').find("li").its('length').should('be.gte', 0); + cy.get('.btn-modal-close').click({force: true}); + }); + + it('Creating a dynamic link and verifying it in a dialog', () => { + get_dialog_with_dynamic_link().as('dialog'); + cy.get_field('doc_type').clear(); + cy.fill_field('doc_type', 'User', 'Link'); + cy.get_field('doc_id').click(); + + //Checking if the listbox have length greater than 0 + cy.get('[data-fieldname="doc_id"]').find('.awesomplete').find("li").its('length').should('be.gte', 0); + cy.get('.btn-modal-close').click({force: true, multiple: true}); + }); + + it('Creating a dynamic link and verifying it', () => { + cy.visit('/app/test-dynamic-link'); + + //Clicking on the Document ID field + cy.get_field('doc_type').clear(); + + //Entering User in the Doctype field + cy.fill_field('doc_type', 'User', 'Link', {delay: 500}); + cy.get_field('doc_id').click(); + + //Checking if the listbox have length greater than 0 + cy.get('[data-fieldname="doc_id"]').find('.awesomplete').find("li").its('length').should('be.gte', 0); + + //Opening a new form for dynamic link doctype + cy.new_form('Test Dynamic Link'); + cy.get_field('doc_type').clear(); + + //Entering User in the Doctype field + cy.fill_field('doc_type', 'User', 'Link', {delay: 500}); + cy.get_field('doc_id').click(); + + //Checking if the listbox have length greater than 0 + cy.get('[data-fieldname="doc_id"]').find('.awesomplete').find("li").its('length').should('be.gte', 0); + cy.get_field('doc_type').clear(); + + //Entering System Settings in the Doctype field + cy.fill_field('doc_type', 'System Settings', 'Link', {delay: 500}); + cy.get_field('doc_id').click(); + + //Checking if the system throws error + cy.get('.modal-title').should('have.text', 'Error'); + cy.get('.msgprint').should('have.text', 'System Settings is not a valid DocType for Dynamic Link'); + }); +}); \ No newline at end of file diff --git a/cypress/support/index.js b/cypress/support/index.js index 9cd770a31e..5980e96677 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -17,6 +17,9 @@ import './commands'; import '@cypress/code-coverage/support'; +Cypress.on('uncaught:exception', (err, runnable) => { + return false; +}); // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/frappe/__init__.py b/frappe/__init__.py index 0abaf932a7..0eca4d99d0 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE """ Frappe - Low Code Open Source Framework in Python and JS @@ -20,10 +20,10 @@ if _dev_server: warnings.simplefilter('always', DeprecationWarning) warnings.simplefilter('always', PendingDeprecationWarning) -from werkzeug.local import Local, release_local import sys, importlib, inspect, json -import typing import click +from werkzeug.local import Local, release_local +from typing import TYPE_CHECKING, Dict, List, Union # Local application imports from .exceptions import * @@ -143,15 +143,14 @@ lang = local("lang") # This if block is never executed when running the code. It is only used for # telling static code analyzer where to find dynamically defined attributes. -if typing.TYPE_CHECKING: - from frappe.utils.redis_wrapper import RedisWrapper - +if TYPE_CHECKING: from frappe.database.mariadb.database import MariaDBDatabase from frappe.database.postgres.database import PostgresDatabase from frappe.query_builder.builder import MariaDB, Postgres + from frappe.utils.redis_wrapper import RedisWrapper - db: typing.Union[MariaDBDatabase, PostgresDatabase] - qb: typing.Union[MariaDB, Postgres] + db: Union[MariaDBDatabase, PostgresDatabase] + qb: Union[MariaDB, Postgres] # end: static analysis hack @@ -897,7 +896,12 @@ def clear_document_cache(doctype, name): cache().hdel('document_cache', key) def get_cached_value(doctype, name, fieldname, as_dict=False): - doc = get_cached_doc(doctype, name) + try: + doc = get_cached_doc(doctype, name) + except DoesNotExistError: + clear_last_message() + return + if isinstance(fieldname, str): if as_dict: throw('Cannot make dict for single fieldname') @@ -1523,12 +1527,16 @@ def get_value(*args, **kwargs): """ return db.get_value(*args, **kwargs) -def as_json(obj, indent=1): +def as_json(obj: Union[Dict, List], indent=1) -> str: from frappe.utils.response import json_handler + try: return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': ')) except TypeError: - return json.dumps(obj, indent=indent, default=json_handler, separators=(',', ': ')) + # this would break in case the keys are not all os "str" type - as defined in the JSON + # adding this to ensure keys are sorted (expected behaviour) + sorted_obj = dict(sorted(obj.items(), key=lambda kv: str(kv[0]))) + return json.dumps(sorted_obj, indent=indent, default=json_handler, separators=(',', ': ')) def are_emails_muted(): from frappe.utils import cint diff --git a/frappe/database/database.py b/frappe/database/database.py index 24dfdd32df..511d993aa5 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -119,8 +119,8 @@ class Database(object): if not run: return query - # remove \n \t from start and end of query - query = re.sub(r'^\s*|\s*$', '', query) + # remove whitespace / indentation from start and end of query + query = query.strip() if re.search(r'ifnull\(', query, flags=re.IGNORECASE): # replaces ifnull in query with coalesce @@ -357,6 +357,7 @@ class Database(object): order_by="KEEP_DEFAULT_ORDERING", cache=False, for_update=False, + *, run=True, pluck=False, distinct=False, @@ -386,17 +387,27 @@ class Database(object): frappe.db.get_value("System Settings", None, "date_format") """ - ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug, + result = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug, order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct, limit=1) if not run: - return ret + return result + + if not result: + return None + + row = result[0] + + if len(row) > 1 or as_dict: + return row + else: + # single field is requested, send it without wrapping in containers + return row[0] - return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False, debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, - run=True, pluck=False, distinct=False, limit=None): + *, run=True, pluck=False, distinct=False, limit=None): """Returns multiple document properties. :param doctype: DocType name. @@ -487,6 +498,7 @@ class Database(object): as_dict=False, debug=False, update=None, + *, run=True, pluck=False, distinct=False, @@ -621,7 +633,8 @@ class Database(object): filters, doctype, as_dict, - debug, + *, + debug=False, order_by=None, update=None, for_update=False, @@ -661,7 +674,7 @@ class Database(object): ) return r - def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False, limit=None): + def _get_value_for_many_names(self, doctype, names, field, order_by, *, debug=False, run=True, pluck=False, distinct=False, limit=None): names = list(filter(None, names)) if names: return self.get_all( diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index 5d04fbe982..267419a887 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -37,7 +37,7 @@ frappe.ui.form.PrintView = class { this.print_wrapper = this.page.main.empty().html( `