Bladeren bron

Merge branch 'develop' into map-control

version-14
Raffael Meyer 3 jaren geleden
committed by GitHub
bovenliggende
commit
2a965df4d0
Geen bekende sleutel gevonden voor deze handtekening in de database GPG sleutel-ID: 4AEE18F83AFDEB23
13 gewijzigde bestanden met toevoegingen van 272 en 38 verwijderingen
  1. +1
    -0
      .mergify.yml
  2. +128
    -0
      cypress/integration/control_dynamic_link.js
  3. +3
    -0
      cypress/support/index.js
  4. +13
    -10
      frappe/__init__.py
  5. +21
    -8
      frappe/database/database.py
  6. +1
    -1
      frappe/printing/page/print/print.js
  7. +1
    -1
      frappe/public/js/frappe/ui/theme_switcher.js
  8. +15
    -0
      frappe/tests/test_db.py
  9. +23
    -9
      frappe/tests/test_translate.py
  10. +15
    -1
      frappe/tests/translation_test_file.txt
  11. +30
    -2
      frappe/translate.py
  12. +19
    -5
      frappe/translations/fr.csv
  13. +2
    -1
      frappe/utils/jinja.py

+ 1
- 0
.mergify.yml Bestand weergeven

@@ -5,6 +5,7 @@ pull_request_rules:
- and:
- author!=surajshetty3416
- author!=gavindsouza
- author!=deepeshgarg007
- or:
- base=version-13
- base=version-12


+ 128
- 0
cypress/integration/control_dynamic_link.js Bestand weergeven

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

+ 3
- 0
cypress/support/index.js Bestand weergeven

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


+ 13
- 10
frappe/__init__.py Bestand weergeven

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


+ 21
- 8
frappe/database/database.py Bestand weergeven

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


+ 1
- 1
frappe/printing/page/print/print.js Bestand weergeven

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


+ 1
- 1
frappe/public/js/frappe/ui/theme_switcher.js Bestand weergeven

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


+ 15
- 0
frappe/tests/test_db.py Bestand weergeven

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


+ 23
- 9
frappe/tests/test_translate.py Bestand weergeven

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


+ 15
- 1
frappe/tests/translation_test_file.txt Bestand weergeven

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

+ 30
- 2
frappe/translate.py Bestand weergeven

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


+ 19
- 5
frappe/translations/fr.csv Bestand weergeven

@@ -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 &#39;{0}&#39;,
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&#39;est modifiable qu
{0}: Other permission rules may also apply,{0}: d&#39;autres règles d&#39;autorisation peuvent également s&#39;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&#39;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&#39;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&#39;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&#39;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&#39;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

+ 2
- 1
frappe/utils/jinja.py Bestand weergeven

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


Laden…
Annuleren
Opslaan