Quellcode durchsuchen

Merge branch 'develop' into discussions-component-redesign

version-14
Jannat Patel vor 3 Jahren
committed by GitHub
Ursprung
Commit
b8a064ca82
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden GPG-Schlüssel-ID: 4AEE18F83AFDEB23
14 geänderte Dateien mit 292 neuen und 50 gelöschten Zeilen
  1. +1
    -0
      .mergify.yml
  2. +128
    -0
      cypress/integration/control_dynamic_link.js
  3. +3
    -0
      cypress/support/index.js
  4. +19
    -11
      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. +14
    -11
      frappe/utils/data.py
  14. +2
    -1
      frappe/utils/jinja.py

+ 1
- 0
.mergify.yml Datei anzeigen

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


+ 128
- 0
cypress/integration/control_dynamic_link.js Datei anzeigen

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

@@ -17,6 +17,9 @@
import './commands'; import './commands';
import '@cypress/code-coverage/support'; import '@cypress/code-coverage/support';


Cypress.on('uncaught:exception', (err, runnable) => {
return false;
});


// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')


+ 19
- 11
frappe/__init__.py Datei anzeigen

@@ -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 # License: MIT. See LICENSE
""" """
Frappe - Low Code Open Source Framework in Python and JS Frappe - Low Code Open Source Framework in Python and JS
@@ -20,10 +20,10 @@ if _dev_server:
warnings.simplefilter('always', DeprecationWarning) warnings.simplefilter('always', DeprecationWarning)
warnings.simplefilter('always', PendingDeprecationWarning) warnings.simplefilter('always', PendingDeprecationWarning)


from werkzeug.local import Local, release_local
import sys, importlib, inspect, json import sys, importlib, inspect, json
import typing
import click import click
from werkzeug.local import Local, release_local
from typing import TYPE_CHECKING, Dict, List, Union


# Local application imports # Local application imports
from .exceptions import * 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 # 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. # 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.mariadb.database import MariaDBDatabase
from frappe.database.postgres.database import PostgresDatabase from frappe.database.postgres.database import PostgresDatabase
from frappe.query_builder.builder import MariaDB, Postgres 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 # end: static analysis hack
@@ -897,7 +896,12 @@ def clear_document_cache(doctype, name):
cache().hdel('document_cache', key) cache().hdel('document_cache', key)


def get_cached_value(doctype, name, fieldname, as_dict=False): 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 isinstance(fieldname, str):
if as_dict: if as_dict:
throw('Cannot make dict for single fieldname') throw('Cannot make dict for single fieldname')
@@ -1523,12 +1527,16 @@ def get_value(*args, **kwargs):
""" """
return db.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 from frappe.utils.response import json_handler

try: try:
return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': ')) return json.dumps(obj, indent=indent, sort_keys=True, default=json_handler, separators=(',', ': '))
except TypeError: 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(): def are_emails_muted():
from frappe.utils import cint from frappe.utils import cint


+ 21
- 8
frappe/database/database.py Datei anzeigen

@@ -119,8 +119,8 @@ class Database(object):
if not run: if not run:
return query 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): if re.search(r'ifnull\(', query, flags=re.IGNORECASE):
# replaces ifnull in query with coalesce # replaces ifnull in query with coalesce
@@ -357,6 +357,7 @@ class Database(object):
order_by="KEEP_DEFAULT_ORDERING", order_by="KEEP_DEFAULT_ORDERING",
cache=False, cache=False,
for_update=False, for_update=False,
*,
run=True, run=True,
pluck=False, pluck=False,
distinct=False, distinct=False,
@@ -386,17 +387,27 @@ class Database(object):
frappe.db.get_value("System Settings", None, "date_format") 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) order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct, limit=1)


if not run: 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, 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, 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. """Returns multiple document properties.


:param doctype: DocType name. :param doctype: DocType name.
@@ -487,6 +498,7 @@ class Database(object):
as_dict=False, as_dict=False,
debug=False, debug=False,
update=None, update=None,
*,
run=True, run=True,
pluck=False, pluck=False,
distinct=False, distinct=False,
@@ -621,7 +633,8 @@ class Database(object):
filters, filters,
doctype, doctype,
as_dict, as_dict,
debug,
*,
debug=False,
order_by=None, order_by=None,
update=None, update=None,
for_update=False, for_update=False,
@@ -661,7 +674,7 @@ class Database(object):
) )
return r 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)) names = list(filter(None, names))
if names: if names:
return self.get_all( return self.get_all(


+ 1
- 1
frappe/printing/page/print/print.js Datei anzeigen

@@ -37,7 +37,7 @@ frappe.ui.form.PrintView = class {
this.print_wrapper = this.page.main.empty().html( this.print_wrapper = this.page.main.empty().html(
`<div class="print-preview-wrapper"><div class="print-preview"> `<div class="print-preview-wrapper"><div class="print-preview">
${frappe.render_template('print_skeleton_loading')} ${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> </iframe>
</div> </div>
<div class="page-break-message text-muted text-center text-medium margin-top"></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 Datei anzeigen

@@ -124,7 +124,7 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher {
toggle_theme(theme) { toggle_theme(theme) {
this.current_theme = theme.toLowerCase(); this.current_theme = theme.toLowerCase();
document.documentElement.setAttribute("data-theme-mode", this.current_theme); 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", { frappe.xcall("frappe.core.doctype.user.user.switch_theme", {
theme: toTitle(theme) theme: toTitle(theme)


+ 15
- 0
frappe/tests/test_db.py Datei anzeigen

@@ -312,6 +312,21 @@ class TestDB(unittest.TestCase):


frappe.db.MAX_WRITES_PER_TRANSACTION = Database.MAX_WRITES_PER_TRANSACTION 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): def test_pk_collision_ignoring(self):
# note has `name` generated from title # note has `name` generated from title
for _ in range(3): for _ in range(3):


+ 23
- 9
frappe/tests/test_translate.py Datei anzeigen

@@ -36,7 +36,18 @@ class TestTranslate(unittest.TestCase):


def test_extract_message_from_file(self): def test_extract_message_from_file(self):
data = frappe.translate.get_messages_from_file(translation_string_file) 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): def test_translation_with_context(self):
try: try:
@@ -107,13 +118,16 @@ class TestTranslate(unittest.TestCase):




expected_output = [ 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 Datei anzeigen

@@ -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.''')
_('''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 Datei anzeigen

@@ -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 frappe.query_builder import Field, DocType
from pypika.terms import PseudoColumn 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: def get_language(lang_list: List = None) -> str:
"""Set `frappe.local.lang` from HTTP headers at beginning of request """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() frappe.clear_last_message()


messages = [] 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') message = m.group('message')
context = m.group('py_context') or m.group('js_context') context = m.group('py_context') or m.group('js_context')
pos = m.start() pos = m.start()


+ 19
- 5
frappe/translations/fr.csv Datei anzeigen

@@ -218,7 +218,7 @@ Route,Route,
Sales Manager,Responsable des Ventes, Sales Manager,Responsable des Ventes,
Sales Master Manager,Directeur des Ventes, Sales Master Manager,Directeur des Ventes,
Sales User,Chargé de Ventes, Sales User,Chargé de Ventes,
Salutation,Salutations,
Salutation,Civilité,
Sample,Échantillon, Sample,Échantillon,
Saturday,Samedi, Saturday,Samedi,
Saved,Enregistré, Saved,Enregistré,
@@ -786,6 +786,7 @@ Custom Sidebar Menu,Barre Latérale Personnalisée,
Custom Translations,Traductions Personnalisées, Custom Translations,Traductions Personnalisées,
Customization,Personnalisation, Customization,Personnalisation,
Customizations Reset,Réinitialiser les Personnalisations, 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}, 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,Personnaliser le formulaire,
Customize Form Field,Personnaliser un Champ de 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 to comment,Connectez-vous pour commenter,
Login token required,Identifiants de Connexion Requis, Login token required,Identifiants de Connexion Requis,
Login with LDAP,Se connecter avec LDAP, Login with LDAP,Se connecter avec LDAP,
Logout,Connectez - Out,
Logout,Déconnecté,
Long Text,Texte Long, 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 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é., 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 for anything,Rechercher tout,
Search in a document type,Rechercher dans un type de document, 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 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..., Search...,Rechercher...,
Searching,Recherche, Searching,Recherche,
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}: 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, {0} Page Views,{0} pages vues,
Expand,Développer, 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».", "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.", "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».", "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 week,depuis la semaine dernière,
since last month,depuis le mois dernier, since last month,depuis le mois dernier,
since last year,depuis l&#39;année dernière, since last year,depuis l&#39;année dernière,
Show,Spectacle,
Show,Afficher,
New Number Card,Nouvelle carte de numéro, New Number Card,Nouvelle carte de numéro,
Your Shortcuts,Vos raccourcis, 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., 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, 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}., 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}, 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

+ 14
- 11
frappe/utils/data.py Datei anzeigen

@@ -1645,18 +1645,21 @@ def validate_json_string(string: str) -> None:
raise frappe.ValidationError raise frappe.ValidationError


def get_user_info_for_avatar(user_id: str) -> Dict: def get_user_info_for_avatar(user_id: str) -> Dict:
user_info = {
"email": user_id,
"image": "",
"name": user_id
}
try: try:
user_info["email"] = frappe.get_cached_value("User", user_id, "email")
user_info["name"] = frappe.get_cached_value("User", user_id, "full_name")
user_info["image"] = frappe.get_cached_value("User", user_id, "user_image")
except Exception:
frappe.local.message_log = []
return user_info
user = frappe.get_cached_doc("User", user_id)
return {
"email": user.email,
"image": user.user_image,
"name": user.full_name
}

except frappe.DoesNotExistError:
frappe.clear_last_message()
return {
"email": user_id,
"image": "",
"name": user_id
}




def validate_python_code(string: str, fieldname=None, is_expression: bool = True) -> None: def validate_python_code(string: str, fieldname=None, is_expression: bool = True) -> None:


+ 2
- 1
frappe/utils/jinja.py Datei anzeigen

@@ -48,7 +48,8 @@ def validate_template(html):
"""Throws exception if there is a syntax error in the Jinja Template""" """Throws exception if there is a syntax error in the Jinja Template"""
import frappe import frappe
from jinja2 import TemplateSyntaxError from jinja2 import TemplateSyntaxError

if not html:
return
jenv = get_jenv() jenv = get_jenv()
try: try:
jenv.from_string(html) jenv.from_string(html)


Laden…
Abbrechen
Speichern