@@ -5,6 +5,7 @@ pull_request_rules: | |||
- and: | |||
- author!=surajshetty3416 | |||
- author!=gavindsouza | |||
- author!=deepeshgarg007 | |||
- or: | |||
- base=version-13 | |||
- base=version-12 | |||
@@ -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'); | |||
}); | |||
}); |
@@ -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') | |||
@@ -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 | |||
@@ -1523,12 +1522,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 | |||
@@ -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( | |||
@@ -37,7 +37,7 @@ frappe.ui.form.PrintView = class { | |||
this.print_wrapper = this.page.main.empty().html( | |||
`<div class="print-preview-wrapper"><div class="print-preview"> | |||
${frappe.render_template('print_skeleton_loading')} | |||
<iframe class="print-format-container" width="100%" height="0" frameBorder="0" scrolling="no""> | |||
<iframe class="print-format-container" width="100%" height="0" frameBorder="0" scrolling="no"> | |||
</iframe> | |||
</div> | |||
<div class="page-break-message text-muted text-center text-medium margin-top"></div> | |||
@@ -124,7 +124,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { | |||
toggle_theme(theme) { | |||
this.current_theme = theme.toLowerCase(); | |||
document.documentElement.setAttribute("data-theme-mode", this.current_theme); | |||
frappe.show_alert("Theme Changed", 3); | |||
frappe.show_alert(__("Theme Changed"), 3); | |||
frappe.xcall("frappe.core.doctype.user.user.switch_theme", { | |||
theme: toTitle(theme) | |||
@@ -312,6 +312,21 @@ class TestDB(unittest.TestCase): | |||
frappe.db.MAX_WRITES_PER_TRANSACTION = Database.MAX_WRITES_PER_TRANSACTION | |||
def test_transaction_write_counting(self): | |||
note = frappe.get_doc(doctype="Note", title="transaction counting").insert() | |||
writes = frappe.db.transaction_writes | |||
frappe.db.set_value("Note", note.name, "content", "abc") | |||
self.assertEqual(1, frappe.db.transaction_writes - writes) | |||
writes = frappe.db.transaction_writes | |||
frappe.db.sql(""" | |||
update `tabNote` | |||
set content = 'abc' | |||
where name = %s | |||
""", note.name) | |||
self.assertEqual(1, frappe.db.transaction_writes - writes) | |||
def test_pk_collision_ignoring(self): | |||
# note has `name` generated from title | |||
for _ in range(3): | |||
@@ -36,7 +36,18 @@ class TestTranslate(unittest.TestCase): | |||
def test_extract_message_from_file(self): | |||
data = frappe.translate.get_messages_from_file(translation_string_file) | |||
self.assertListEqual(data, expected_output) | |||
exp_filename = "apps/frappe/frappe/tests/translation_test_file.txt" | |||
self.assertEqual(len(data), len(expected_output), | |||
msg=f"Mismatched output:\nExpected: {expected_output}\nFound: {data}") | |||
for extracted, expected in zip(data, expected_output): | |||
ext_filename, ext_message, ext_context, ext_line = extracted | |||
exp_message, exp_context, exp_line = expected | |||
self.assertEqual(ext_filename, exp_filename) | |||
self.assertEqual(ext_message, exp_message) | |||
self.assertEqual(ext_context, exp_context) | |||
self.assertEqual(ext_line, exp_line) | |||
def test_translation_with_context(self): | |||
try: | |||
@@ -107,13 +118,16 @@ class TestTranslate(unittest.TestCase): | |||
expected_output = [ | |||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', 'This is some context', 2), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', None, 4), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', "You don't have any messages yet.", None, 6), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Submit', 'Some DocType', 8), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', 'This is some context', 15), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Submit', 'Some DocType', 17), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', "You don't have any messages yet.", None, 19), | |||
('apps/frappe/frappe/tests/translation_test_file.txt', "You don't have any messages yet.", None, 21) | |||
('Warning: Unable to find {0} in any table related to {1}', 'This is some context', 2), | |||
('Warning: Unable to find {0} in any table related to {1}', None, 4), | |||
("You don't have any messages yet.", None, 6), | |||
('Submit', 'Some DocType', 8), | |||
('Warning: Unable to find {0} in any table related to {1}', 'This is some context', 15), | |||
('Submit', 'Some DocType', 17), | |||
("You don't have any messages yet.", None, 19), | |||
("You don't have any messages yet.", None, 21), | |||
("Long string that needs its own line because of black formatting.", None, 24), | |||
("Long string with", "context", 28), | |||
("Long string with", "context on newline", 32), | |||
] | |||
@@ -18,4 +18,18 @@ _('Submit', context="Some DocType") | |||
_("""You don't have any messages yet.""") | |||
_('''You don't have any messages yet.''') | |||
_('''You don't have any messages yet.''') | |||
// allow newline in beginning | |||
_( | |||
"""Long string that needs its own line because of black formatting.""" | |||
).format("blah") | |||
_( | |||
"Long string with", context="context" | |||
).format("blah") | |||
_( | |||
"Long string with", | |||
context="context on newline" | |||
).format("blah") |
@@ -23,6 +23,35 @@ from frappe.utils import get_bench_path, is_html, strip, strip_html_tags | |||
from frappe.query_builder import Field, DocType | |||
from pypika.terms import PseudoColumn | |||
TRANSLATE_PATTERN = re.compile( | |||
r"_\([\s\n]*" # starts with literal `_(`, ignore following whitespace/newlines | |||
# BEGIN: message search | |||
r"([\"']{,3})" # start of message string identifier - allows: ', ", """, '''; 1st capture group | |||
r"(?P<message>((?!\1).)*)" # Keep matching until string closing identifier is met which is same as 1st capture group | |||
r"\1" # match exact string closing identifier | |||
# END: message search | |||
# BEGIN: python context search | |||
r"([\s\n]*,[\s\n]*context\s*=\s*" # capture `context=` with ignoring whitespace | |||
r"([\"'])" # start of context string identifier; 5th capture group | |||
r"(?P<py_context>((?!\5).)*)" # capture context string till closing id is found | |||
r"\5" # match context string closure | |||
r")?" # match 0 or 1 context strings | |||
# END: python context search | |||
# BEGIN: JS context search | |||
r"(\s*,\s*(.)*?\s*(,\s*" # skip message format replacements: ["format", ...] | null | [] | |||
r"([\"'])" # start of context string; 11th capture group | |||
r"(?P<js_context>((?!\11).)*)" # capture context string till closing id is found | |||
r"\11" # match context string closure | |||
r")*" | |||
r")*" # match one or more context string | |||
# END: JS context search | |||
r"[\s\n]*\)" # Closing function call ignore leading whitespace/newlines | |||
) | |||
def get_language(lang_list: List = None) -> str: | |||
"""Set `frappe.local.lang` from HTTP headers at beginning of request | |||
@@ -651,9 +680,8 @@ def extract_messages_from_code(code): | |||
frappe.clear_last_message() | |||
messages = [] | |||
pattern = r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)" | |||
for m in re.compile(pattern).finditer(code): | |||
for m in TRANSLATE_PATTERN.finditer(code): | |||
message = m.group('message') | |||
context = m.group('py_context') or m.group('js_context') | |||
pos = m.start() | |||
@@ -218,7 +218,7 @@ Route,Route, | |||
Sales Manager,Responsable des Ventes, | |||
Sales Master Manager,Directeur des Ventes, | |||
Sales User,Chargé de Ventes, | |||
Salutation,Salutations, | |||
Salutation,Civilité, | |||
Sample,Échantillon, | |||
Saturday,Samedi, | |||
Saved,Enregistré, | |||
@@ -786,6 +786,7 @@ Custom Sidebar Menu,Barre Latérale Personnalisée, | |||
Custom Translations,Traductions Personnalisées, | |||
Customization,Personnalisation, | |||
Customizations Reset,Réinitialiser les Personnalisations, | |||
Reset Customizations,Réinitialiser les Personnalisations, | |||
Customizations for <b>{0}</b> exported to:<br>{1},Personnalisations pour <b>{0}</b> exportées vers: <br> {1}, | |||
Customize Form,Personnaliser le formulaire, | |||
Customize Form Field,Personnaliser un Champ de Formulaire, | |||
@@ -1508,7 +1509,7 @@ Login not allowed at this time,Connexion non autorisée pour le moment, | |||
Login to comment,Connectez-vous pour commenter, | |||
Login token required,Identifiants de Connexion Requis, | |||
Login with LDAP,Se connecter avec LDAP, | |||
Logout,Connectez - Out, | |||
Logout,Déconnecté, | |||
Long Text,Texte Long, | |||
Looks like something is wrong with this site's Paypal configuration.,Il semble qu'il y ait une erreur avec la configuration Paypal de ce site., | |||
Looks like something is wrong with this site's payment gateway configuration. No payment has been made.,On dirait que quelque chose ne va pas dans la configuration de la passerelle de paiement de ce site. Aucun paiement n'a été effectué., | |||
@@ -2164,7 +2165,7 @@ Search for '{0}',Rechercher '{0}', | |||
Search for anything,Rechercher tout, | |||
Search in a document type,Rechercher dans un type de document, | |||
Search or Create a New Chat,Rechercher ou créer un nouveau chat, | |||
Search or type a command,Rechercher ou taper une commande, | |||
Search or type a command (Ctrl + G),Rechercher ou taper une commande (Ctrl + G), | |||
Search...,Rechercher..., | |||
Searching,Recherche, | |||
Searching ...,Recherche ..., | |||
@@ -4139,7 +4140,7 @@ Document is only editable by users with role,Le document n'est modifiable qu | |||
{0}: Other permission rules may also apply,{0}: d'autres règles d'autorisation peuvent également s'appliquer, | |||
{0} Page Views,{0} pages vues, | |||
Expand,Développer, | |||
Collapse,Effondrer, | |||
Collapse,Réduire, | |||
"Invalid Bearer token, please provide a valid access token with prefix 'Bearer'.","Jeton de porteur non valide, veuillez fournir un jeton d'accès valide avec le préfixe «porteur».", | |||
"Failed to decode token, please provide a valid base64-encoded token.","Échec du décodage du jeton, veuillez fournir un jeton encodé en base64 valide.", | |||
"Invalid token, please provide a valid token with prefix 'Basic' or 'Token'.","Jeton non valide, veuillez fournir un jeton valide avec le préfixe «Basic» ou «Token».", | |||
@@ -4215,7 +4216,7 @@ since yesterday,depuis hier, | |||
since last week,depuis la semaine dernière, | |||
since last month,depuis le mois dernier, | |||
since last year,depuis l'année dernière, | |||
Show,Spectacle, | |||
Show,Afficher, | |||
New Number Card,Nouvelle carte de numéro, | |||
Your Shortcuts,Vos raccourcis, | |||
You haven't added any Dashboard Charts or Number Cards yet.,Vous n'avez pas encore ajouté de tableaux de bord ou de cartes numériques., | |||
@@ -4700,3 +4701,16 @@ Value cannot be negative for {0}: {1},La valeur ne peut pas être négative pour | |||
Negative Value,Valeur négative, | |||
Authentication failed while receiving emails from Email Account: {0}.,L'authentification a échoué lors de la réception des e-mails du compte de messagerie: {0}., | |||
Message from server: {0},Message du serveur: {0}, | |||
{0} edited this {1},{0} a édité {1}, | |||
{0} created this {1}, {0} a créé {1} | |||
Report an Issue, Signaler une anomalie | |||
About, A Propos | |||
My Profile, Mon profil | |||
My Settings, Mes paramètres | |||
Toggle Full Width, Changer l'affichage en pleine largeur | |||
Toggle Theme, Basculer le thème | |||
Theme Changed, Thème changé | |||
Amend, Nouv. version | |||
Document has been submitted, Document validé | |||
Document has been cancelled, Document annulé | |||
Document is in draft state, Document au statut brouillon |
@@ -48,7 +48,8 @@ def validate_template(html): | |||
"""Throws exception if there is a syntax error in the Jinja Template""" | |||
import frappe | |||
from jinja2 import TemplateSyntaxError | |||
if not html: | |||
return | |||
jenv = get_jenv() | |||
try: | |||
jenv.from_string(html) | |||