@@ -13,7 +13,7 @@ context('Awesome Bar', () => { | |||
it('navigates to doctype list', () => { | |||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('todo', { delay: 700 }); | |||
cy.get('.awesomplete').findByRole('listbox').should('be.visible'); | |||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('{downarrow}{enter}', { delay: 700 }); | |||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)').type('{enter}', { delay: 700 }); | |||
cy.get('.title-text').should('contain', 'To Do'); | |||
@@ -22,7 +22,7 @@ context('Awesome Bar', () => { | |||
it('find text in doctype list', () => { | |||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)') | |||
.type('test in todo{downarrow}{enter}', { delay: 700 }); | |||
.type('test in todo{enter}', { delay: 700 }); | |||
cy.get('.title-text').should('contain', 'To Do'); | |||
@@ -32,7 +32,7 @@ context('Awesome Bar', () => { | |||
it('navigates to new form', () => { | |||
cy.findByPlaceholderText('Search or type a command (Ctrl + G)') | |||
.type('new blog post{downarrow}{enter}', { delay: 700 }); | |||
.type('new blog post{enter}', { delay: 700 }); | |||
cy.get('.title-text:visible').should('have.text', 'New Blog Post'); | |||
}); | |||
@@ -0,0 +1,90 @@ | |||
context('Attach Control', () => { | |||
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 Attach Control', | |||
fields: [ | |||
{ | |||
"label": "Attach File or Image", | |||
"fieldname": "attach", | |||
"fieldtype": "Attach", | |||
"in_list_view": 1, | |||
}, | |||
] | |||
}); | |||
}); | |||
}); | |||
it('Checking functionality for "Link" button in the "Attach" fieldtype', () => { | |||
//Navigating to the new form for the newly created doctype | |||
cy.new_form('Test Attach Control'); | |||
//Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype | |||
cy.findByRole('button', {name: 'Attach'}).click(); | |||
//Clicking on "Link" button to attach a file using the "Link" button | |||
cy.findByRole('button', {name: 'Link'}).click(); | |||
cy.findByPlaceholderText('Attach a web link').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg'); | |||
//Clicking on the Upload button to upload the file | |||
cy.intercept("POST", "/api/method/upload_file").as("upload_image"); | |||
cy.get('.modal-footer').findByRole("button", {name: "Upload"}).click({delay: 500}); | |||
cy.wait("@upload_image"); | |||
cy.findByRole('button', {name: 'Save'}).click(); | |||
//Checking if the URL of the attached image is getting displayed in the field of the newly created doctype | |||
cy.get('.attached-file > .ellipsis > .attached-file-link') | |||
.should('have.attr', 'href') | |||
.and('equal', 'https://wallpaperplay.com/walls/full/8/2/b/72402.jpg'); | |||
//Clicking on the "Clear" button | |||
cy.get('[data-action="clear_attachment"]').click(); | |||
//Checking if clicking on the clear button clears the field of the doctype form and again displays the attach button | |||
cy.get('.control-input > .btn-sm').should('contain', 'Attach'); | |||
//Deleting the doc | |||
cy.go_to_list('Test Attach Control'); | |||
cy.get('.list-row-checkbox').eq(0).click(); | |||
cy.get('.actions-btn-group > .btn').contains('Actions').click(); | |||
cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click(); | |||
cy.click_modal_primary_button('Yes'); | |||
}); | |||
it('Checking functionality for "Library" button in the "Attach" fieldtype', () => { | |||
//Navigating to the new form for the newly created doctype | |||
cy.new_form('Test Attach Control'); | |||
//Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype | |||
cy.findByRole('button', {name: 'Attach'}).click(); | |||
//Clicking on "Library" button to attach a file using the "Library" button | |||
cy.findByRole('button', {name: 'Library'}).click(); | |||
cy.contains('72402.jpg').click(); | |||
//Clicking on the Upload button to upload the file | |||
cy.intercept("POST", "/api/method/upload_file").as("upload_image"); | |||
cy.get('.modal-footer').findByRole("button", {name: "Upload"}).click({delay: 500}); | |||
cy.wait("@upload_image"); | |||
cy.findByRole('button', {name: 'Save'}).click(); | |||
//Checking if the URL of the attached image is getting displayed in the field of the newly created doctype | |||
cy.get('.attached-file > .ellipsis > .attached-file-link') | |||
.should('have.attr', 'href') | |||
.and('equal', 'https://wallpaperplay.com/walls/full/8/2/b/72402.jpg'); | |||
//Clicking on the "Clear" button | |||
cy.get('[data-action="clear_attachment"]').click(); | |||
//Checking if clicking on the clear button clears the field of the doctype form and again displays the attach button | |||
cy.get('.control-input > .btn-sm').should('contain', 'Attach'); | |||
//Deleting the doc | |||
cy.go_to_list('Test Attach Control'); | |||
cy.get('.list-row-checkbox').eq(0).click(); | |||
cy.get('.actions-btn-group > .btn').contains('Actions').click(); | |||
cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click(); | |||
cy.click_modal_primary_button('Yes'); | |||
}); | |||
}); |
@@ -0,0 +1,71 @@ | |||
context('Date Control', () => { | |||
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 Date Control', | |||
fields: [ | |||
{ | |||
"label": "Date", | |||
"fieldname": "date", | |||
"fieldtype": "Date", | |||
"in_list_view": 1 | |||
}, | |||
] | |||
}); | |||
}); | |||
}); | |||
it('Selecting a date from the datepicker', () => { | |||
cy.new_form('Test Date Control'); | |||
cy.get_field('date', 'Date').click(); | |||
cy.get('.datepicker--nav-title').click(); | |||
cy.get('.datepicker--nav-title').click({force: true}); | |||
//Inputing values in the date field | |||
cy.get('.datepicker--years > .datepicker--cells > .datepicker--cell[data-year=2020]').click(); | |||
cy.get('.datepicker--months > .datepicker--cells > .datepicker--cell[data-month=0]').click(); | |||
cy.get('.datepicker--days > .datepicker--cells > .datepicker--cell[data-date=15]').click(); | |||
//Verifying if the selected date is displayed in the date field | |||
cy.get_field('date', 'Date').should('have.value', '01-15-2020'); | |||
}); | |||
it('Checking next and previous button', () => { | |||
cy.get_field('date', 'Date').click(); | |||
//Clicking on the next button in the datepicker | |||
cy.get('.datepicker--nav-action[data-action=next]').click(); | |||
//Selecting a date from the datepicker | |||
cy.get('.datepicker--cell[data-date=15]').click({force: true}); | |||
//Verifying if the selected date has been displayed in the date field | |||
cy.get_field('date', 'Date').should('have.value', '02-15-2020'); | |||
cy.wait(500); | |||
cy.get_field('date', 'Date').click(); | |||
//Clicking on the previous button in the datepicker | |||
cy.get('.datepicker--nav-action[data-action=prev]').click(); | |||
//Selecting a date from the datepicker | |||
cy.get('.datepicker--cell[data-date=15]').click({force: true}); | |||
//Verifying if the selected date has been displayed in the date field | |||
cy.get_field('date', 'Date').should('have.value', '01-15-2020'); | |||
}); | |||
it('Clicking on "Today" button gives todays date', () => { | |||
cy.get_field('date', 'Date').click(); | |||
//Clicking on "Today" button | |||
cy.get('.datepicker--button').click(); | |||
//Picking up the todays date | |||
const todays_date = Cypress.moment().format('MM-DD-YYYY'); | |||
//Verifying if clicking on "Today" button matches today's date | |||
cy.get_field('date', 'Date').should('have.value', todays_date); | |||
}); | |||
}); |
@@ -49,7 +49,7 @@ | |||
"fieldname": "doctype_event", | |||
"fieldtype": "Select", | |||
"label": "DocType Event", | |||
"options": "Before Insert\nBefore Validate\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" | |||
"options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" | |||
}, | |||
{ | |||
"depends_on": "eval:doc.script_type==='API'", | |||
@@ -109,10 +109,11 @@ | |||
"link_fieldname": "server_script" | |||
} | |||
], | |||
"modified": "2021-09-04 12:02:43.671240", | |||
"modified": "2022-04-07 19:41:23.178772", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Server Script", | |||
"naming_rule": "Set by user", | |||
"owner": "Administrator", | |||
"permissions": [ | |||
{ | |||
@@ -130,5 +131,6 @@ | |||
], | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [], | |||
"track_changes": 1 | |||
} |
@@ -1,7 +1,7 @@ | |||
{ | |||
"actions": [ | |||
{ | |||
"action": "#List/Console Log/List", | |||
"action": "app/console-log", | |||
"action_type": "Route", | |||
"label": "Logs" | |||
}, | |||
@@ -86,7 +86,7 @@ | |||
"index_web_pages_for_search": 1, | |||
"issingle": 1, | |||
"links": [], | |||
"modified": "2021-09-15 17:17:44.844767", | |||
"modified": "2022-04-09 16:35:32.345542", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "System Console", | |||
@@ -104,5 +104,6 @@ | |||
"quick_entry": 1, | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [], | |||
"track_changes": 1 | |||
} |
@@ -222,7 +222,7 @@ def install_app(name, verbose=False, set_as_patched=True): | |||
# install pre-requisites | |||
if app_hooks.required_apps: | |||
for app in app_hooks.required_apps: | |||
name = parse_app_name(name) | |||
name = parse_app_name(app) | |||
install_app(name, verbose=verbose) | |||
frappe.flags.in_install = name | |||
@@ -132,32 +132,30 @@ class BaseDocument(object): | |||
def get_db_value(self, key): | |||
return frappe.db.get_value(self.doctype, self.name, key) | |||
def get(self, key=None, filters=None, limit=None, default=None): | |||
if key: | |||
if isinstance(key, dict): | |||
return _filter(self.get_all_children(), key, limit=limit) | |||
if filters: | |||
if isinstance(filters, dict): | |||
value = _filter(self.__dict__.get(key, []), filters, limit=limit) | |||
else: | |||
default = filters | |||
filters = None | |||
value = self.__dict__.get(key, default) | |||
def get(self, key, filters=None, limit=None, default=None): | |||
if isinstance(key, dict): | |||
return _filter(self.get_all_children(), key, limit=limit) | |||
if filters: | |||
if isinstance(filters, dict): | |||
value = _filter(self.__dict__.get(key, []), filters, limit=limit) | |||
else: | |||
default = filters | |||
filters = None | |||
value = self.__dict__.get(key, default) | |||
else: | |||
value = self.__dict__.get(key, default) | |||
if value is None and key in ( | |||
d.fieldname for d in self.meta.get_table_fields() | |||
): | |||
value = [] | |||
self.set(key, value) | |||
if value is None and key in ( | |||
d.fieldname for d in self.meta.get_table_fields() | |||
): | |||
value = [] | |||
self.set(key, value) | |||
if limit and isinstance(value, (list, tuple)) and len(value) > limit: | |||
value = value[:limit] | |||
if limit and isinstance(value, (list, tuple)) and len(value) > limit: | |||
value = value[:limit] | |||
return value | |||
else: | |||
return self.__dict__ | |||
return value | |||
def getone(self, key, filters=None): | |||
return self.get(key, filters=filters, limit=1)[0] | |||
@@ -476,7 +476,7 @@ class DatabaseQuery(object): | |||
if 'ifnull(' in f.fieldname: | |||
column_name = self.cast_name(f.fieldname, "ifnull(") | |||
else: | |||
column_name = self.cast_name(f"{tname}.{f.fieldname}") | |||
column_name = self.cast_name(f"{tname}.`{f.fieldname}`") | |||
if f.operator.lower() in additional_filters_config: | |||
f.update(get_additional_filter_field(additional_filters_config, f, f.value)) | |||
@@ -45,7 +45,7 @@ def export_customizations(module, doctype, sync_on_migrate=0, with_permissions=0 | |||
if not frappe.get_conf().developer_mode: | |||
raise Exception('Not developer mode') | |||
custom = {'custom_fields': [], 'property_setters': [], 'custom_perms': [], | |||
custom = {'custom_fields': [], 'property_setters': [], 'custom_perms': [],'links':[], | |||
'doctype': doctype, 'sync_on_migrate': sync_on_migrate} | |||
def add(_doctype): | |||
@@ -53,6 +53,8 @@ def export_customizations(module, doctype, sync_on_migrate=0, with_permissions=0 | |||
fields='*', filters={'dt': _doctype}) | |||
custom['property_setters'] += frappe.get_all('Property Setter', | |||
fields='*', filters={'doc_type': _doctype}) | |||
custom['links'] += frappe.get_all('DocType Link', | |||
fields='*', filters={'parent': _doctype}) | |||
add(doctype) | |||
@@ -44,6 +44,8 @@ frappe.ui.form.Control = class BaseControl { | |||
} | |||
if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) { | |||
let status = "Write"; | |||
// like in case of a dialog box | |||
if (cint(this.df.hidden)) { | |||
// eslint-disable-next-line | |||
@@ -55,10 +57,10 @@ frappe.ui.form.Control = class BaseControl { | |||
if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console | |||
return "None"; | |||
} else if (cint(this.df.read_only || this.df.is_virtual)) { | |||
} else if (cint(this.df.read_only || this.df.is_virtual || this.df.fieldtype === "Read Only")) { | |||
// eslint-disable-next-line | |||
if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console | |||
return "Read"; | |||
status = "Read"; | |||
} else if ((this.grid && | |||
this.grid.display_status == 'Read') || | |||
@@ -67,10 +69,16 @@ frappe.ui.form.Control = class BaseControl { | |||
this.layout.grid.display_status == 'Read')) { | |||
// parent grid is read | |||
if (explain) console.log("By Parent Grid Read-only: Read"); // eslint-disable-line no-console | |||
return "Read"; | |||
status = "Read"; | |||
} | |||
return "Write"; | |||
if ( | |||
status === "Read" && | |||
is_null(this.value) && | |||
!in_list(["HTML", "Image", "Button"], this.df.fieldtype) | |||
) status = "None"; | |||
return status; | |||
} | |||
var status = frappe.perm.get_field_display_status(this.df, | |||
@@ -23,7 +23,6 @@ import './table'; | |||
import './color'; | |||
import './signature'; | |||
import './password'; | |||
import './read_only'; | |||
import './button'; | |||
import './html'; | |||
import './markdown_editor'; | |||
@@ -262,3 +262,5 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp | |||
return this.grid || this.layout && this.layout.grid; | |||
} | |||
}; | |||
frappe.ui.form.ControlReadOnly = frappe.ui.form.ControlData; |
@@ -58,7 +58,7 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f | |||
})); | |||
this.add_non_group_layers(data_layers, this.editableLayers); | |||
try { | |||
this.map.flyToBounds(this.editableLayers.getBounds(), { | |||
this.map.fitBounds(this.editableLayers.getBounds(), { | |||
padding: [50,50] | |||
}); | |||
} | |||
@@ -66,10 +66,10 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f | |||
// suppress error if layer has a point. | |||
} | |||
this.editableLayers.addTo(this.map); | |||
this.map._onResize(); | |||
} else if ((value===undefined) || (value == JSON.stringify(new L.FeatureGroup().toGeoJSON()))) { | |||
this.locate_control.start(); | |||
} else { | |||
this.map.setView(frappe.utils.map_defaults.center, frappe.utils.map_defaults.zoom); | |||
} | |||
this.map.invalidateSize(); | |||
} | |||
bind_leaflet_map() { | |||
@@ -97,8 +97,7 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f | |||
}); | |||
L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/'; | |||
this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center, | |||
frappe.utils.map_defaults.zoom); | |||
this.map = L.map(this.map_id); | |||
L.tileLayer(frappe.utils.map_defaults.tiles, | |||
frappe.utils.map_defaults.options).addTo(this.map); | |||
@@ -146,9 +145,8 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f | |||
}; | |||
// create control and add to map | |||
var drawControl = new L.Control.Draw(options); | |||
this.map.addControl(drawControl); | |||
this.drawControl = new L.Control.Draw(options); | |||
this.map.addControl(this.drawControl); | |||
this.map.on('draw:created', (e) => { | |||
var type = e.layerType, | |||
@@ -1,8 +0,0 @@ | |||
frappe.ui.form.ControlReadOnly = class ControlReadOnly extends frappe.ui.form.ControlData { | |||
get_status(explain) { | |||
var status = super.get_status(explain); | |||
if(status==="Write") | |||
status = "Read"; | |||
return; | |||
} | |||
}; |
@@ -225,7 +225,10 @@ $.extend(frappe.perm, { | |||
if (explain) console.log("By Workflow:" + status); | |||
// read only field is checked | |||
if (status === "Write" && cint(df.read_only)) { | |||
if (status === "Write" && ( | |||
cint(df.read_only) || | |||
df.fieldtype === "Read Only" | |||
)) { | |||
status = "Read"; | |||
} | |||
if (explain) console.log("By Read Only:" + status); | |||
@@ -276,4 +279,4 @@ $.extend(frappe.perm, { | |||
return allowed_docs; | |||
} | |||
} | |||
}); | |||
}); |
@@ -22,17 +22,15 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { | |||
super.make(); | |||
this.refresh(); | |||
// set default | |||
$.each(this.fields_list, function(i, field) { | |||
if (field.df["default"]) { | |||
let def_value = field.df["default"]; | |||
$.each(this.fields_list, (_, field) => { | |||
if (!is_null(field.df.default)) { | |||
let def_value = field.df.default; | |||
if (def_value == 'Today' && field.df["fieldtype"] == 'Date') { | |||
if (def_value === "Today" && field.df.fieldtype === "Date") { | |||
def_value = frappe.datetime.get_today(); | |||
} | |||
field.set_input(def_value); | |||
// if default and has depends_on, render its fields. | |||
me.refresh_dependency(); | |||
this.set_value(field.df.fieldname, def_value); | |||
} | |||
}) | |||
@@ -129,6 +127,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout { | |||
if (f) { | |||
f.set_value(val).then(() => { | |||
f.set_input(val); | |||
f.refresh(); | |||
this.refresh_dependency(); | |||
resolve(); | |||
}); | |||
@@ -0,0 +1,191 @@ | |||
// LICENSE | |||
// | |||
// This software is dual-licensed to the public domain and under the following | |||
// license: you are granted a perpetual, irrevocable license to copy, modify, | |||
// publish, and distribute this file as you see fit. | |||
// | |||
// VERSION | |||
// 0.1.0 (2016-03-28) Initial release | |||
// | |||
// AUTHOR | |||
// Forrest Smith | |||
// | |||
// CONTRIBUTORS | |||
// J�rgen Tjern� - async helper | |||
// Anurag Awasthi - updated to 0.2.0 | |||
const SEQUENTIAL_BONUS = 15; // bonus for adjacent matches | |||
const SEPARATOR_BONUS = 30; // bonus if match occurs after a separator | |||
const CAMEL_BONUS = 30; // bonus if match is uppercase and prev is lower | |||
const FIRST_LETTER_BONUS = 15; // bonus if the first letter is matched | |||
const LEADING_LETTER_PENALTY = -5; // penalty applied for every letter in str before the first match | |||
const MAX_LEADING_LETTER_PENALTY = -15; // maximum penalty for leading letters | |||
const UNMATCHED_LETTER_PENALTY = -1; | |||
/** | |||
* Does a fuzzy search to find pattern inside a string. | |||
* @param {*} pattern string pattern to search for | |||
* @param {*} str string string which is being searched | |||
* @returns [boolean, number] a boolean which tells if pattern was | |||
* found or not and a search score | |||
*/ | |||
export function fuzzy_match(pattern, str) { | |||
const recursion_count = 0; | |||
const recursion_limit = 10; | |||
const matches = []; | |||
const max_matches = 256; | |||
return fuzzy_match_recursive( | |||
pattern, | |||
str, | |||
0 /* pattern_cur_index */, | |||
0 /* str_curr_index */, | |||
null /* src_matches */, | |||
matches, | |||
max_matches, | |||
0 /* next_match */, | |||
recursion_count, | |||
recursion_limit | |||
); | |||
} | |||
function fuzzy_match_recursive( | |||
pattern, | |||
str, | |||
pattern_cur_index, | |||
str_curr_index, | |||
src_matches, | |||
matches, | |||
max_matches, | |||
next_match, | |||
recursion_count, | |||
recursion_limit | |||
) { | |||
let out_score = 0; | |||
// Return if recursion limit is reached. | |||
if (++recursion_count >= recursion_limit) { | |||
return [false, out_score]; | |||
} | |||
// Return if we reached ends of strings. | |||
if (pattern_cur_index === pattern.length || str_curr_index === str.length) { | |||
return [false, out_score]; | |||
} | |||
// Recursion params | |||
let recursive_match = false; | |||
let best_recursive_matches = []; | |||
let best_recursive_score = 0; | |||
// Loop through pattern and str looking for a match. | |||
let first_match = true; | |||
while (pattern_cur_index < pattern.length && str_curr_index < str.length) { | |||
// Match found. | |||
if ( | |||
pattern[pattern_cur_index].toLowerCase() === str[str_curr_index].toLowerCase() | |||
) { | |||
if (next_match >= max_matches) { | |||
return [false, out_score]; | |||
} | |||
if (first_match && src_matches) { | |||
matches = [...src_matches]; | |||
first_match = false; | |||
} | |||
const recursive_matches = []; | |||
const [matched, recursive_score] = fuzzy_match_recursive( | |||
pattern, | |||
str, | |||
pattern_cur_index, | |||
str_curr_index + 1, | |||
matches, | |||
recursive_matches, | |||
max_matches, | |||
next_match, | |||
recursion_count, | |||
recursion_limit | |||
); | |||
if (matched) { | |||
// Pick best recursive score. | |||
if (!recursive_match || recursive_score > best_recursive_score) { | |||
best_recursive_matches = [...recursive_matches]; | |||
best_recursive_score = recursive_score; | |||
} | |||
recursive_match = true; | |||
} | |||
matches[next_match++] = str_curr_index; | |||
++pattern_cur_index; | |||
} | |||
++str_curr_index; | |||
} | |||
const matched = pattern_cur_index === pattern.length; | |||
if (matched) { | |||
out_score = 100; | |||
// Apply leading letter penalty | |||
let penalty = LEADING_LETTER_PENALTY * matches[0]; | |||
penalty = | |||
penalty < MAX_LEADING_LETTER_PENALTY | |||
? MAX_LEADING_LETTER_PENALTY | |||
: penalty; | |||
out_score += penalty; | |||
//Apply unmatched penalty | |||
const unmatched = str.length - next_match; | |||
out_score += UNMATCHED_LETTER_PENALTY * unmatched; | |||
// Apply ordering bonuses | |||
for (let i = 0; i < next_match; i++) { | |||
const curr_idx = matches[i]; | |||
if (i > 0) { | |||
const prev_idx = matches[i - 1]; | |||
if (curr_idx == prev_idx + 1) { | |||
out_score += SEQUENTIAL_BONUS; | |||
} | |||
} | |||
// Check for bonuses based on neighbor character value. | |||
if (curr_idx > 0) { | |||
// Camel case | |||
const neighbor = str[curr_idx - 1]; | |||
const curr = str[curr_idx]; | |||
if ( | |||
neighbor !== neighbor.toUpperCase() && | |||
curr !== curr.toLowerCase() | |||
) { | |||
out_score += CAMEL_BONUS; | |||
} | |||
const is_neighbour_separator = neighbor == "_" || neighbor == " "; | |||
if (is_neighbour_separator) { | |||
out_score += SEPARATOR_BONUS; | |||
} | |||
} else { | |||
// First letter | |||
out_score += FIRST_LETTER_BONUS; | |||
} | |||
} | |||
// Return best result | |||
if (recursive_match && (!matched || best_recursive_score > out_score)) { | |||
// Recursive score is better than "this" | |||
matches = [...best_recursive_matches]; | |||
out_score = best_recursive_score; | |||
return [true, out_score]; | |||
} else if (matched) { | |||
// "this" score is better than recursive | |||
return [true, out_score]; | |||
} else { | |||
return [false, out_score]; | |||
} | |||
} | |||
return [false, out_score]; | |||
} |
@@ -1,4 +1,6 @@ | |||
frappe.provide('frappe.search'); | |||
import { fuzzy_match } from './fuzzy_match.js'; | |||
frappe.search.utils = { | |||
setup_recent: function() { | |||
@@ -533,101 +535,46 @@ frappe.search.utils = { | |||
}, | |||
fuzzy_search: function(keywords, _item) { | |||
// Returns 10 for case-perfect contain, 0 for not found | |||
// 9 for perfect contain, | |||
// 0 - 6 for fuzzy contain | |||
// **Specific use-case step** | |||
keywords = keywords || ''; | |||
var item = __(_item || ''); | |||
var item_without_hyphen = item.replace(/-/g, " "); | |||
var item_length = item.length; | |||
var query_length = keywords.length; | |||
var length_ratio = query_length / item_length; | |||
var max_skips = 3, max_mismatch_len = 2; | |||
if (query_length > item_length) { | |||
return 0; | |||
} | |||
// check for perfect string matches or | |||
// matches that start with the keyword | |||
if ([item, item_without_hyphen].includes(keywords) | |||
|| [item, item_without_hyphen].some((txt) => txt.toLowerCase().indexOf(keywords) === 0)) { | |||
return 10 + length_ratio; | |||
} | |||
if (item.indexOf(keywords) !== -1 && keywords !== keywords.toLowerCase()) { | |||
return 9 + length_ratio; | |||
} | |||
item = item.toLowerCase(); | |||
keywords = keywords.toLowerCase(); | |||
if (item.indexOf(keywords) !== -1) { | |||
return 8 + length_ratio; | |||
} | |||
var skips = 0, mismatches = 0; | |||
outer: for (var i = 0, j = 0; i < query_length; i++) { | |||
if (mismatches !== 0) skips++; | |||
if (skips > max_skips) return 0; | |||
var k_ch = keywords.charCodeAt(i); | |||
mismatches = 0; | |||
while (j < item_length) { | |||
if (item.charCodeAt(j++) === k_ch) { | |||
continue outer; | |||
} | |||
if(++mismatches > max_mismatch_len) return 0 ; | |||
} | |||
return 0; | |||
} | |||
// Since indexOf didn't pass, there will be atleast 1 skip | |||
// hence no divide by zero, but just to be safe | |||
if((skips + mismatches) > 0) { | |||
return (5 + length_ratio)/(skips + mismatches); | |||
} else { | |||
return 0; | |||
} | |||
var match = fuzzy_match(keywords, item); | |||
return match[1]; | |||
}, | |||
bolden_match_part: function(str, subseq) { | |||
var rendered = ""; | |||
if(this.fuzzy_search(subseq, str) === 0) { | |||
if (fuzzy_match(subseq, str)[0] === false) { | |||
return str; | |||
} else if(this.fuzzy_search(subseq, str) > 6) { | |||
var regEx = new RegExp("("+ subseq +")", "ig"); | |||
return str.replace(regEx, '<mark>$1</mark>'); | |||
} else { | |||
var str_orig = str; | |||
var str = str.toLowerCase(); | |||
var str_len = str.length; | |||
var subseq = subseq.toLowerCase(); | |||
outer: for(var i = 0, j = 0; i < subseq.length; i++) { | |||
var sub_ch = subseq.charCodeAt(i); | |||
while(j < str_len) { | |||
if(str.charCodeAt(j) === sub_ch) { | |||
var str_char = str_orig.charAt(j); | |||
if(str_char === str_char.toLowerCase()) { | |||
rendered += '<mark>' + subseq.charAt(i) + '</mark>'; | |||
} else { | |||
rendered += '<mark>' + subseq.charAt(i).toUpperCase() + '</mark>'; | |||
} | |||
j++; | |||
continue outer; | |||
} | |||
if (str.indexOf(subseq) == 0) { | |||
var tail = str.split(subseq)[1]; | |||
return '<mark>' + subseq + '</mark>' + tail; | |||
} | |||
var rendered = ""; | |||
var str_orig = str; | |||
var str_len = str.length; | |||
str = str.toLowerCase(); | |||
subseq = subseq.toLowerCase(); | |||
outer: for (var i = 0, j = 0; i < subseq.length; i++) { | |||
var sub_ch = subseq.charCodeAt(i); | |||
while (j < str_len) { | |||
if (str.charCodeAt(j) === sub_ch) { | |||
var str_char = str_orig.charAt(j); | |||
if (str_char === str_char.toLowerCase()) { | |||
rendered += '<mark>' + subseq.charAt(i) + '</mark>'; | |||
} else { | |||
rendered += '<mark>' + subseq.charAt(i).toUpperCase() + '</mark>'; | |||
} | |||
rendered += str_orig.charAt(j); | |||
j++; | |||
continue outer; | |||
} | |||
return str_orig; | |||
rendered += str_orig.charAt(j); | |||
j++; | |||
} | |||
rendered += str_orig.slice(j); | |||
return rendered; | |||
return str_orig; | |||
} | |||
rendered += str_orig.slice(j); | |||
return rendered; | |||
}, | |||
get_executables(keywords) { | |||
@@ -73,6 +73,7 @@ | |||
display: inline-block; | |||
width: 100%; | |||
height: 100%; | |||
object-fit: cover; | |||
background-color: var(--avatar-frame-bg); | |||
background-size: cover; | |||
background-repeat: no-repeat; | |||
@@ -145,6 +146,7 @@ | |||
.standard-image { | |||
width: 100%; | |||
height: 100%; | |||
object-fit: cover; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
@@ -51,11 +51,6 @@ | |||
} | |||
} | |||
.custom-actions { | |||
display: flex; | |||
align-items: center; | |||
} | |||
.page-actions { | |||
align-items: center; | |||
.btn { | |||
@@ -72,6 +67,11 @@ | |||
.custom-btn-group { | |||
display: inline-flex; | |||
} | |||
.custom-actions { | |||
display: flex; | |||
align-items: center; | |||
} | |||
} | |||
.layout-main-section-wrapper { | |||
@@ -1,13 +1,3 @@ | |||
$font-size-xs: 0.7rem; | |||
$font-size-sm: 0.85rem; | |||
$font-size-lg: 1.12rem; | |||
$font-size-xl: 1.25rem; | |||
$font-size-2xl: 1.5rem; | |||
$font-size-3xl: 2rem; | |||
$font-size-4xl: 2.5rem; | |||
$font-size-5xl: 3rem; | |||
$font-size-6xl: 4rem; | |||
html { | |||
height: 100%; | |||
} | |||
@@ -29,68 +19,67 @@ h1, h2, h3, h4 { | |||
} | |||
h1 { | |||
font-size: $font-size-3xl; | |||
font-size: 2rem; | |||
line-height: 1.25; | |||
letter-spacing: -0.025em; | |||
margin-top: 3rem; | |||
margin-bottom: 0.75rem; | |||
@include media-breakpoint-up(sm) { | |||
font-size: $font-size-5xl; | |||
line-height: 2.5rem; | |||
font-size: 2.5rem; | |||
margin-top: 3.5rem; | |||
margin-bottom: 1.25rem; | |||
} | |||
@include media-breakpoint-up(xl) { | |||
font-size: $font-size-6xl; | |||
font-size: 3.5rem; | |||
line-height: 1; | |||
margin-top: 4rem; | |||
} | |||
} | |||
h2 { | |||
font-size: $font-size-2xl; | |||
font-size: 1.4rem; | |||
margin-top: 2rem; | |||
margin-bottom: 0.75rem; | |||
margin-bottom: 0.5rem; | |||
@include media-breakpoint-up(sm) { | |||
font-size: $font-size-3xl; | |||
font-size: 2rem; | |||
margin-top: 4rem; | |||
margin-bottom: 1rem; | |||
margin-bottom: 0.75rem; | |||
} | |||
@include media-breakpoint-up(xl) { | |||
font-size: $font-size-4xl; | |||
font-size: 2.5rem; | |||
margin-top: 4rem; | |||
} | |||
} | |||
h3 { | |||
font-size: $font-size-xl; | |||
margin-top: 1.5rem; | |||
font-size: 1.2rem; | |||
margin-top: 2rem; | |||
margin-bottom: 0.5rem; | |||
@include media-breakpoint-up(sm) { | |||
font-size: $font-size-2xl; | |||
font-size: 1.4rem; | |||
margin-top: 2.5rem; | |||
} | |||
@include media-breakpoint-up(xl) { | |||
font-size: $font-size-3xl; | |||
font-size: 1.9rem; | |||
margin-top: 3.5rem; | |||
} | |||
} | |||
h4 { | |||
font-size: $font-size-lg; | |||
margin-top: 1rem; | |||
font-size: 1.1rem; | |||
margin-top: 2rem; | |||
margin-bottom: 0.5rem; | |||
@include media-breakpoint-up(sm) { | |||
font-size: $font-size-xl; | |||
margin-top: 1.25rem; | |||
font-size: 1.3rem; | |||
margin-top: 2.5rem; | |||
} | |||
@include media-breakpoint-up(xl) { | |||
font-size: $font-size-2xl; | |||
margin-top: 1.75rem; | |||
font-size: 1.5rem; | |||
margin-top: 3rem; | |||
} | |||
a { | |||
@@ -98,6 +87,10 @@ h4 { | |||
} | |||
} | |||
p { | |||
line-height: 1.7; | |||
} | |||
.btn.btn-lg { | |||
font-size: $font-size-lg; | |||
font-size: 1.1rem; | |||
} |
@@ -14,6 +14,10 @@ | |||
} | |||
} | |||
.blog-list-content { | |||
margin-bottom: 3rem; | |||
} | |||
.blog-card { | |||
margin-bottom: 2rem; | |||
position: relative; | |||
@@ -98,10 +102,15 @@ | |||
.blog-header { | |||
margin-bottom: 3rem; | |||
margin-top: 3rem; | |||
margin-top: 5rem; | |||
} | |||
} | |||
.blog-comments { | |||
margin-top: 1rem; | |||
margin-bottom: 5rem; | |||
} | |||
.feedback-item svg { | |||
vertical-align: sub; | |||
@@ -1,4 +1,5 @@ | |||
.error-page { | |||
margin: 3rem 0; | |||
text-align: center; | |||
.img-404 { | |||
@@ -1,5 +1,5 @@ | |||
.web-footer { | |||
margin: 5rem 0; | |||
padding: 3rem 0; | |||
min-height: 140px; | |||
background-color: var(--fg-color); | |||
border-top: 1px solid $border-color; | |||
@@ -114,8 +114,8 @@ | |||
@media (max-width: map-get($grid-breakpoints, "lg")) { | |||
.page-content-wrapper .container { | |||
padding-left: 1rem; | |||
padding-right: 1rem; | |||
padding-left: 1.5rem; | |||
padding-right: 1.5rem; | |||
} | |||
} | |||
@@ -5,7 +5,6 @@ | |||
} | |||
.from-markdown { | |||
color: $gray-700; | |||
line-height: 1.7; | |||
> :first-child { | |||
@@ -30,7 +29,15 @@ | |||
} | |||
p, li { | |||
font-size: $font-size-lg; | |||
line-height: 1.7; | |||
@include media-breakpoint-up(sm) { | |||
font-size: 1.05rem; | |||
} | |||
} | |||
p.lead { | |||
@extend .lead; | |||
} | |||
li { | |||
@@ -16,16 +16,18 @@ | |||
} | |||
} | |||
.hero-title, .hero-subtitle { | |||
max-width: 42rem; | |||
margin-top: 0rem; | |||
margin-bottom: 0.5rem; | |||
} | |||
.lead { | |||
color: var(--text-muted); | |||
font-weight: normal; | |||
font-size: 1.25rem; | |||
margin-top: -0.5rem; | |||
margin-bottom: 1.5rem; | |||
@include media-breakpoint-up(sm) { | |||
margin-top: -1rem; | |||
margin-bottom: 2.5rem; | |||
} | |||
} | |||
.hero-subtitle { | |||
@@ -38,6 +40,12 @@ | |||
} | |||
} | |||
.hero-title, .hero-subtitle { | |||
max-width: 42rem; | |||
margin-top: 0rem; | |||
margin-bottom: 0.5rem; | |||
} | |||
.hero.align-center { | |||
h1, .hero-title, .hero-subtitle, .hero-buttons { | |||
text-align: center; | |||
@@ -51,6 +59,7 @@ | |||
.section-description { | |||
max-width: 56rem; | |||
color: var(--text-muted); | |||
margin-top: 0.5rem; | |||
font-size: $font-size-lg; | |||
@@ -549,7 +558,7 @@ | |||
font-weight: 600; | |||
@include media-breakpoint-up(md) { | |||
font-size: $font-size-2xl; | |||
font-size: $font-size-xl; | |||
} | |||
} | |||
@@ -1,14 +1,18 @@ | |||
{% from "frappe/templates/includes/avatar_macro.html" import avatar %} | |||
<div class="comment-row media my-5"> | |||
<div class="my-5 comment-row media"> | |||
<div class="comment-avatar"> | |||
{{ avatar(user_id=(comment.comment_email or comment.sender), size='avatar-medium') }} | |||
{{ avatar(user_id=(frappe.utils.strip_html(comment.comment_email or comment.sender)), size='avatar-medium') }} | |||
</div> | |||
<div class="comment-content"> | |||
<div class="head mb-2"> | |||
<span class="title font-weight-bold mr-2">{{ comment.sender_full_name or comment.comment_by }}</span> | |||
<span class="time small text-muted">{{ frappe.utils.pretty_date(comment.creation) }}</span> | |||
<div class="mb-2 head"> | |||
<span class="mr-2 title font-weight-bold"> | |||
{{ frappe.utils.strip_html(comment.sender_full_name or comment.comment_by) | e }} | |||
</span> | |||
<span class="time small text-muted"> | |||
{{ frappe.utils.pretty_date(comment.creation) }} | |||
</span> | |||
</div> | |||
<div class="content">{{ comment.content | markdown }}</div> | |||
<div class="content">{{ frappe.utils.strip_html(comment.content) | markdown }}</div> | |||
</div> | |||
</div> |
@@ -507,13 +507,40 @@ class TestReportview(unittest.TestCase): | |||
if frappe.db.db_type == "postgres": | |||
self.assertTrue("strpos( cast( \"tabautoinc_dt_test\".\"name\" as varchar), \'1\')" in query) | |||
self.assertTrue("where cast(\"tabautoinc_dt_test\".name as varchar) = \'1\'" in query) | |||
self.assertTrue("where cast(\"tabautoinc_dt_test\".\"name\" as varchar) = \'1\'" in query) | |||
else: | |||
self.assertTrue("locate(\'1\', `tabautoinc_dt_test`.`name`)" in query) | |||
self.assertTrue("where `tabautoinc_dt_test`.name = 1" in query) | |||
self.assertTrue("where `tabautoinc_dt_test`.`name` = 1" in query) | |||
dt.delete(ignore_permissions=True) | |||
def test_fieldname_starting_with_int(self): | |||
from frappe.core.doctype.doctype.test_doctype import new_doctype | |||
dt = new_doctype( | |||
"dt_with_int_named_fieldname", | |||
fields=[{ | |||
"label": "1field", | |||
"fieldname": "1field", | |||
"fieldtype": "Int" | |||
}] | |||
).insert(ignore_permissions=True) | |||
frappe.get_doc({ | |||
"doctype": "dt_with_int_named_fieldname", | |||
"1field": 10 | |||
}).insert(ignore_permissions=True) | |||
query = DatabaseQuery("dt_with_int_named_fieldname") | |||
self.assertTrue(query.execute(filters={"1field": 10})) | |||
self.assertTrue(query.execute(filters={"1field": ["like", "1%"]})) | |||
self.assertTrue(query.execute(filters={"1field": ["in", "1,2,10"]})) | |||
self.assertTrue(query.execute(filters={"1field": ["is", "set"]})) | |||
self.assertFalse(query.execute(filters={"1field": ["not like", "1%"]})) | |||
dt.delete() | |||
def add_child_table_to_blog_post(): | |||
child_table = frappe.get_doc({ | |||
@@ -293,7 +293,7 @@ old_parent,grand_parent, | |||
(Ctrl + G),(Ctrl + G), | |||
** Failed: {0} to {1}: {2},** Échec: {0} à {1}: {2}, | |||
**Currency** Master,Données de Base **Devise**, | |||
0 - Draft; 1 - Submitted; 2 - Cancelled,0 - Brouillon; 1 - Soumis; 2 - Annulé, | |||
0 - Draft; 1 - Submitted; 2 - Cancelled,0 - Brouillon; 1 - Validé; 2 - Annulé, | |||
0 is highest,0 est le plus élevé, | |||
1 Currency = [?] Fraction\nFor e.g. 1 USD = 100 Cent,1 Devise = [?] Fraction \nE.g. 1 USD = 100 centimes, | |||
1 comment,1 commentaire, | |||
@@ -377,7 +377,7 @@ Align Labels to the Right,Alignez les Étiquettes à Droite, | |||
Align Value,Aligner la Valeur, | |||
All Images attached to Website Slideshow should be public,Toutes les images jointes au diaporama du site Web doivent être publiques, | |||
All customizations will be removed. Please confirm.,Toutes les personnalisations seront supprimées. Veuillez confirmer., | |||
"All possible Workflow States and roles of the workflow. Docstatus Options: 0 is""Saved"", 1 is ""Submitted"" and 2 is ""Cancelled""","Tous les États et Rôles possibles du Flux de Travail. Options de Statut du Document : 0 est ""Enregistré"", 1 est ""Soumis"" et 2 est ""Annulé""", | |||
"All possible Workflow States and roles of the workflow. Docstatus Options: 0 is""Saved"", 1 is ""Submitted"" and 2 is ""Cancelled""","Tous les États et Rôles possibles du Flux de Travail. Options de Statut du Document : 0 est ""Enregistré"", 1 est ""Validé"" et 2 est ""Annulé""", | |||
All-uppercase is almost as easy to guess as all-lowercase.,Tout en majuscules est presque aussi facile à deviner que tout en minuscules., | |||
Allocated To,Attribué à, | |||
Allow,Autoriser, | |||
@@ -404,7 +404,7 @@ Allow Self Approval,Autoriser l'auto-approbation, | |||
Allow approval for creator of the document,Autoriser l'approbation par le créateur du document, | |||
Allow events in timeline,Autoriser les événements dans la chronologie, | |||
Allow in Quick Entry,Autoriser dans les entrées rapides, | |||
Allow on Submit,Autoriser à la Soumission, | |||
Allow on Submit,Autoriser à la Validation, | |||
Allow only one session per user,Autoriser une seule session par utilisateur, | |||
Allow page break inside tables,Autoriser les sauts de page dans les tables, | |||
Allow saving if mandatory fields are not filled,Autoriser l'enregistrement si les champs obligatoires ne sont pas remplis, | |||
@@ -594,7 +594,7 @@ Cancelled Document restored as Draft,Le document annulé a été restauré en ta | |||
Cancelling,Annulation, | |||
Cancelling {0},Annulation de {0}, | |||
Cannot Remove,Ne peut être retiré, | |||
Cannot cancel before submitting. See Transition {0},Impossible d'annuler avant de soumettre. Voir Transition {0}, | |||
Cannot cancel before submitting. See Transition {0},Impossible d'annuler avant de valider. Voir Transition {0}, | |||
Cannot change docstatus from 0 to 2,Impossible de changer le statut du document de 0 à 2, | |||
Cannot change docstatus from 1 to 0,Impossible de changer le statut du document de 1 à 0, | |||
Cannot change header content,Impossible de changer le contenu de l'en-tête, | |||
@@ -627,7 +627,7 @@ Card Details,Détails de la carte, | |||
Categorize blog posts.,Catégoriser les posts de blog., | |||
Category Description,Description de la Catégorie, | |||
Cent,Centime, | |||
"Certain documents, like an Invoice, should not be changed once final. The final state for such documents is called Submitted. You can restrict which roles can Submit.","Certains documents, comme une Facture, ne devraient pas être modifiés une fois finalisés. L'état final de ces documents est appelée Soumis. Vous pouvez limiter les rôles pouvant Soumettre.", | |||
"Certain documents, like an Invoice, should not be changed once final. The final state for such documents is called Submitted. You can restrict which roles can Submit.","Certains documents, comme une Facture, ne devraient pas être modifiés une fois finalisés. L'état final de ces documents est appelée Validé. Vous pouvez limiter les rôles pouvant Valider.", | |||
Chain Integrity,Intégrité de la chaîne, | |||
Chaining Hash,Hachage de chaînage, | |||
Change Label (via Custom Translation),Modifier le libellé (via Traduction Personnalisée ), | |||
@@ -896,7 +896,7 @@ DocType <b>{0}</b> provided for the field <b>{1}</b> must have atleast one Link | |||
DocType can not be merged,DocType ne peut pas être fusionné, | |||
DocType can only be renamed by Administrator,DocType ne peut être renommé que par l'Administrateur, | |||
DocType is a Table / Form in the application.,DocType est un Tableau / Formulaire dans l'application., | |||
DocType must be Submittable for the selected Doc Event,Le DocType doit être soumissible pour l'événement Doc sélectionné, | |||
DocType must be Submittable for the selected Doc Event,Le DocType doit être validable pour l'événement Doc sélectionné, | |||
DocType on which this Workflow is applicable.,DocType pour lequel ce Flux de Travail est applicable., | |||
"DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores","Le nom du DocType doit commencer par une lettre et il peut uniquement se composer de lettres, des chiffres, d’espaces et du tiret bas (underscore)", | |||
Doctype required,Doctype requis, | |||
@@ -908,7 +908,7 @@ Document Restored,Document Restauré, | |||
Document Share Report,Rapport de Partage de Document, | |||
Document States,États du Document, | |||
Document Type is not importable,Le type de document n'est pas importable, | |||
Document Type is not submittable,Le type de document n'est pas soumis, | |||
Document Type is not submittable,Le type de document n'est pas valider, | |||
Document Type to Track,Type de document à suivre, | |||
Document Types,Types de documents, | |||
Document can't saved.,Le document ne peut pas être enregistré., | |||
@@ -1392,7 +1392,7 @@ Is Published Field must be a valid fieldname,Le Champ Publié doit-il être un n | |||
Is Single,Est Seul, | |||
Is Spam,Est Spam, | |||
Is Standard,Est Standard, | |||
Is Submittable,Est Soumissible, | |||
Is Submittable,Est Validable, | |||
Is Table,Est Table, | |||
Is Your Company Address,Est l'Adresse de votre Entreprise, | |||
It is risky to delete this file: {0}. Please contact your System Manager.,Il est risqué de supprimer ce fichier : {0}. Veuillez contactez votre Administrateur Système., | |||
@@ -1541,7 +1541,7 @@ Max Value,Valeur Max, | |||
Max width for type Currency is 100px in row {0},Largeur max pour le type Devise est 100px dans la ligne {0}, | |||
Maximum Attachment Limit for this record reached.,Taille maximale des Pièces Jointes pour cet enregistrement est atteint., | |||
Maximum {0} rows allowed,Maximum {0} lignes autorisés, | |||
"Meaning of Submit, Cancel, Amend","Signification de Soumettre, Annuler, Modifier", | |||
"Meaning of Submit, Cancel, Amend","Signification de Valider, Annuler, Modifier", | |||
Mention transaction completion page URL,Mentionnez la page URL de fin de transaction, | |||
Mentions,Mentions, | |||
Menu,Menu, | |||
@@ -1737,7 +1737,7 @@ Old Password,Ancien Mot De Passe, | |||
Old Password Required.,Ancien Mot de Passe Requis., | |||
Older backups will be automatically deleted,Les anciennes sauvegardes seront automatiquement supprimées, | |||
"On {0}, {1} wrote:","Sur {0}, {1} a écrit :", | |||
"Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended.","Une fois soumis, les documents à soumettre ne peuvent plus être modifiés. Ils ne peuvent être annulés et amendés.", | |||
"Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended.","Une fois validé, les documents à valider ne peuvent plus être modifiés. Ils ne peuvent être annulés et amendés.", | |||
"Once you have set this, the users will only be able access documents (eg. Blog Post) where the link exists (eg. Blogger).","Une fois que vous avez défini ceci, les utilisateurs ne pourront accèder qu'aux documents (e.g. Article de Blog) où le lien existe (e.g. Blogger) .", | |||
One Last Step,Une Dernière Étape, | |||
One Time Password (OTP) Registration Code from {},Code de Mot de Passe Unique (OTP) à partir de {}, | |||
@@ -1829,7 +1829,7 @@ Percent Complete,Pourcentage d'Avancement, | |||
Perm Level,Niveau d'Autorisation, | |||
Permanent,Permanent, | |||
Permanently Cancel {0}?,Annuler de Manière Permanente {0} ?, | |||
Permanently Submit {0}?,Soumettre de Manière Permanente {0} ?, | |||
Permanently Submit {0}?,Valider de Manière Permanente {0} ?, | |||
Permanently delete {0}?,Supprimer de Manière Permanente {0} ?, | |||
Permission Error,Erreur d'autorisation, | |||
Permission Level,Niveau d'Autorisation, | |||
@@ -1837,7 +1837,7 @@ Permission Levels,Niveaux d'Autorisation, | |||
Permission Rules,Règles d'Autorisation, | |||
Permissions,Autorisations, | |||
Permissions are automatically applied to Standard Reports and searches.,Les autorisations sont automatiquement appliquées aux rapports standard et aux recherches., | |||
"Permissions are set on Roles and Document Types (called DocTypes) by setting rights like Read, Write, Create, Delete, Submit, Cancel, Amend, Report, Import, Export, Print, Email and Set User Permissions.","Les Autorisations sont définies sur les Rôles et les Types de Documents (appelés DocTypes) en définissant des droits , tels que Lire, Écrire, Créer, Supprimer, Soumettre, Annuler, Modifier, Rapporter, Importer, Exporter, Imprimer, Envoyer un Email et Définir les Autorisations de l'Utilisateur .", | |||
"Permissions are set on Roles and Document Types (called DocTypes) by setting rights like Read, Write, Create, Delete, Submit, Cancel, Amend, Report, Import, Export, Print, Email and Set User Permissions.","Les Autorisations sont définies sur les Rôles et les Types de Documents (appelés DocTypes) en définissant des droits , tels que Lire, Écrire, Créer, Supprimer, Valider, Annuler, Modifier, Rapporter, Importer, Exporter, Imprimer, Envoyer un Email et Définir les Autorisations de l'Utilisateur .", | |||
Permissions at higher levels are Field Level permissions. All Fields have a Permission Level set against them and the rules defined at that permissions apply to the field. This is useful in case you want to hide or make certain field read-only for certain Roles.,Les Autorisations aux niveaux supérieurs sont des permissions de Niveau Champ. Un Niveau d'Autorisation est défini pour chaque Champ et les règles définies pour ces Autorisations s’appliquent au Champ. Ceci est utile si vous voulez cacher ou mettre certains champs en lecture seule pour certains Rôles., | |||
"Permissions at level 0 are Document Level permissions, i.e. they are primary for access to the document.","Les Autorisations au niveau 0 sont les autorisations de Niveau Document, c’est à dire qu'elles sont nécessaires pour accéder au document.", | |||
Permissions get applied on Users based on what Roles they are assigned.,Autorisations sont appliqués aux utilisateurs en fonction des Rôles qui leurs sont affectés., | |||
@@ -2123,7 +2123,7 @@ Row No,Rangée No, | |||
Row Status,État de la ligne, | |||
Row Values Changed,Valeurs de Lignes Modifiées, | |||
Row {0}: Not allowed to disable Mandatory for standard fields,Ligne {0}: impossible de désactiver Obligatoire pour les champs standard, | |||
Row {0}: Not allowed to enable Allow on Submit for standard fields,Ligne {0} : Il n’est pas autorisé d’activer Autoriser à la Soumission pour les champs standards, | |||
Row {0}: Not allowed to enable Allow on Submit for standard fields,Ligne {0} : Il n’est pas autorisé d’activer Autoriser à la Validation pour les champs standards, | |||
Rows Added,Lignes Ajoutées, | |||
Rows Removed,Lignes Supprimées, | |||
Rule,Règle, | |||
@@ -2395,13 +2395,13 @@ Stylesheets for Print Formats,Feuilles de style pour les Formats d'Impression, | |||
Sub-domain provided by erpnext.com,Sous-domaine fourni par erpnext.com, | |||
Subdomain,Sous-domaine, | |||
Subject Field,Champ de sujet, | |||
Submit after importing,Soumettre après l'import, | |||
Submit an Issue,Soumettre un ticket, | |||
Submit this document to confirm,Soumettre ce document pour confirmer, | |||
Submit {0} documents?,Soumettre {0} documents ?, | |||
Submiting {0},Soumission de {0}, | |||
Submitted Document cannot be converted back to draft. Transition row {0},Document Soumis ne peut pas être reconvertis en Brouillon. Ligne de transition {0}, | |||
Submitting,Soumission, | |||
Submit after importing,Valider après l'import, | |||
Submit an Issue,Valider un ticket, | |||
Submit this document to confirm,Valider ce document pour confirmer, | |||
Submit {0} documents?,Valider {0} documents ?, | |||
Submiting {0},Validation de {0}, | |||
Submitted Document cannot be converted back to draft. Transition row {0},Document Valider ne peut pas être reconvertis en Brouillon. Ligne de transition {0}, | |||
Submitting,Validation, | |||
Subscription Notification,Notification d'abonnement, | |||
Subsidiary,Filiale, | |||
Success Action,Action de succès, | |||
@@ -2784,7 +2784,7 @@ You are not permitted to view the newsletter.,Vous n'êtes pas autorisé à | |||
You are now following this document. You will receive daily updates via email. You can change this in User Settings.,Vous suivez maintenant ce document. Vous recevrez des mises à jour quotidiennes par courrier électronique. Vous pouvez modifier cela dans les paramètres de l'utilisateur., | |||
You can add dynamic properties from the document by using Jinja templating.,Vous pouvez ajouter des propriétés dynamiques au document à l'aide des modèles Jinja., | |||
You can also copy-paste this ,Vous pouvez également copier-coller cette, | |||
"You can change Submitted documents by cancelling them and then, amending them.","Vous pouvez modifier les documents Soumis en les annulant et ensuite, en les modifiant.", | |||
"You can change Submitted documents by cancelling them and then, amending them.","Vous pouvez modifier les documents Validés en les annulant et ensuite, en les modifiant.", | |||
You can find things by asking 'find orange in customers',Vous pouvez trouver des choses en demandant 'trouver orange dans clients', | |||
You can only upload upto 5000 records in one go. (may be less in some cases),Vous pouvez seulement charger jusqu'à 5000 enregistrement en une seule fois. (peut-être moins dans certains cas), | |||
You can use Customize Form to set levels on fields.,Vous pouvez utiliser Personaliser le Formulaire pour définir les niveaux de champs., | |||
@@ -2807,7 +2807,7 @@ You gained {0} points,Vous avez gagné {0} points, | |||
You have a new message from: ,Vous avez un nouveau message de:, | |||
You have been successfully logged out,Vous avez été déconnecté avec succès, | |||
You have unsaved changes in this form. Please save before you continue.,Vous avez des modifications non enregistrées dans ce formulaire. Veuillez enregistrer avant de continuer., | |||
You must login to submit this form,Vous devez vous connecter pour soumettre ce formulaire, | |||
You must login to submit this form,Vous devez vous connecter pour valider ce formulaire, | |||
You need to be in developer mode to edit a Standard Web Form,Vous devez être en Mode Développeur pour modifier un Formulaire Web Standard, | |||
You need to be logged in and have System Manager Role to be able to access backups.,Vous devez être connecté et avoir le Role Responsable Système pour pouvoir accéder aux sauvegardes., | |||
You need to be logged in to access this {0}.,Vous devez être connecté pour accéder à ce(tte) {0}., | |||
@@ -2820,7 +2820,7 @@ Your Language,Votre Langue, | |||
Your Name,Votre Nom, | |||
Your account has been locked and will resume after {0} seconds,Votre compte a été verrouillé et reprendra après {0} secondes, | |||
Your connection request to Google Calendar was successfully accepted,Votre demande de connexion à Google Agenda a été acceptée avec succès, | |||
Your information has been submitted,Vos informations ont été soumises, | |||
Your information has been submitted,Vos informations ont été validées, | |||
Your login id is,Votre id de connexion est, | |||
Your organization name and address for the email footer.,Le nom de votre société et l'adresse pour le pied de l'email., | |||
Your payment has been successfully registered.,Votre paiement a été enregistré avec succès., | |||
@@ -2982,7 +2982,7 @@ star,étoile, | |||
star-empty,étoile-vide, | |||
step-backward,vers-larrière, | |||
step-forward,vers-l'avant, | |||
submitted this document,a soumis ce document, | |||
submitted this document,a validé ce document, | |||
text in document type,Texte dans le type de document, | |||
text-height,Hauteur-texte, | |||
text-width,largeur-text, | |||
@@ -3094,11 +3094,11 @@ zoom-out,Réduire, | |||
"{0}, Row {1}","{0}, Ligne {1}", | |||
"{0}: '{1}' ({3}) will get truncated, as max characters allowed is {2}",{0} : {1} '({3}) sera tronqué car le nombre de caractères max est {2}, | |||
{0}: Cannot set Amend without Cancel,{0} : Impossible de choisir Modifier sans Annuler, | |||
{0}: Cannot set Assign Amend if not Submittable,{0} : Impossible de définir ‘Assigner Modifier’ si non Soumissible, | |||
{0}: Cannot set Assign Submit if not Submittable,{0} : Impossible de définir ‘Assigner Soumettre’ si non Soumissible, | |||
{0}: Cannot set Cancel without Submit,{0} : Impossible de choisir Annuler sans Soumettre, | |||
{0}: Cannot set Assign Amend if not Submittable,{0} : Impossible de définir ‘Assigner Modifier’ si non Validable, | |||
{0}: Cannot set Assign Submit if not Submittable,{0} : Impossible de définir ‘Assigner Valider’ si non Validable, | |||
{0}: Cannot set Cancel without Submit,{0} : Impossible de choisir Annuler sans Valider, | |||
{0}: Cannot set Import without Create,{0} : Impossible de choisir Import sans Créer, | |||
"{0}: Cannot set Submit, Cancel, Amend without Write","{0} : Vous ne pouvez pas choisir Envoyer, Annuler, Modifier sans Écrire", | |||
"{0}: Cannot set Submit, Cancel, Amend without Write","{0} : Vous ne pouvez pas choisir Valider, Annuler, Modifier sans Écrire", | |||
{0}: Cannot set import as {1} is not importable,{0} : Impossible de choisir import car {1} n'est pas importable, | |||
{0}: No basic permissions set,{0} : Aucune autorisation de base définie, | |||
"{0}: Only one rule allowed with the same Role, Level and {1}","{0} : Une seule règle est permise avec le même Rôle, Niveau et {1}", | |||
@@ -3153,8 +3153,8 @@ Administration,Administration, | |||
After Cancel,Après annuler, | |||
After Delete,Après la suppression, | |||
After Save,Après l'enregistrement, | |||
After Save (Submitted Document),Après l'enregistrement (document soumis), | |||
After Submit,Après soumettre, | |||
After Save (Submitted Document),Après l'enregistrement (document valider), | |||
After Submit,Après validation, | |||
Aggregate Function Based On,Fonction d'agrégation basée sur, | |||
Aggregate Function field is required to create a dashboard chart,Le champ Fonction d'agrégation est requis pour créer un graphique de tableau de bord, | |||
All Records,Tous les enregistrements, | |||
@@ -3199,8 +3199,8 @@ Before Cancel,Avant d'annuler, | |||
Before Delete,Avant de supprimer, | |||
Before Insert,Avant l'insertion, | |||
Before Save,Avant de sauvegarder, | |||
Before Save (Submitted Document),Avant de sauvegarder (document soumis), | |||
Before Submit,Avant de soumettre, | |||
Before Save (Submitted Document),Avant de sauvegarder (document valider), | |||
Before Submit,Avant de valider, | |||
Blank Template,Modèle vierge, | |||
Callback URL,URL de rappel, | |||
Cancel All Documents,Annuler tous les documents, | |||
@@ -3556,11 +3556,11 @@ Skipping column {0},Colonne ignorée {0}, | |||
Social Home,Maison sociale, | |||
Some columns might get cut off when printing to PDF. Try to keep number of columns under 10.,Certaines colonnes peuvent être coupées lors de l'impression au format PDF. Essayez de garder le nombre de colonnes sous 10., | |||
Something went wrong during the token generation. Click on {0} to generate a new one.,Quelque chose s'est mal passé pendant la génération de jetons. Cliquez sur {0} pour en générer un nouveau., | |||
Submit After Import,Soumettre après importation, | |||
Submitting...,Soumission..., | |||
Submit After Import,Validation après importation, | |||
Submitting...,Validation..., | |||
Success! You are good to go 👍,Succès! Vous êtes bon pour aller, | |||
Successful Transactions,Transactions réussies, | |||
Successfully Submitted!,Soumis avec succès!, | |||
Successfully Submitted!,Validation avec succès!, | |||
Successfully imported {0} record.,{0} enregistrement importé avec succès., | |||
Successfully imported {0} records.,{0} enregistrements importés avec succès., | |||
Successfully updated {0} record.,{0} enregistrement mis à jour avec succès., | |||
@@ -3659,7 +3659,7 @@ choose an,choisir un, | |||
empty,vide, | |||
of,de, | |||
or attach a,ou attacher un, | |||
submitted this document {0},a soumis ce document {0}, | |||
submitted this document {0},a validé ce document {0}, | |||
"tag name..., e.g. #tag","nom de tag ..., par exemple #tag", | |||
uploaded file,fichier téléchargé, | |||
via Data Import,via importation de données, | |||
@@ -3678,7 +3678,7 @@ via Data Import,via importation de données, | |||
{0} shared a document {1} {2} with you,{0} a partagé un document {1} {2} avec vous, | |||
{0} should not be same as {1},{0} ne doit pas être identique à {1}, | |||
{0} translations pending,{0} traductions en attente, | |||
{0} {1} is linked with the following submitted documents: {2},{0} {1} est lié aux documents soumis suivants: {2}, | |||
{0} {1} is linked with the following submitted documents: {2},{0} {1} est lié aux documents validés suivants: {2}, | |||
"{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings","{0}: Impossible de joindre un nouveau document récurrent. Pour activer la pièce jointe dans l'e-mail de notification de répétition automatique, activez {1} dans Paramètres d'impression", | |||
{0}: Fieldname cannot be one of {1},{0}: le nom de champ ne peut pas être l'un des {1}, | |||
{} Complete,{} Achevée, | |||
@@ -3793,7 +3793,7 @@ Sr,Sr, | |||
Start,Démarrer, | |||
Start Time,Heure de Début, | |||
Status,Statut, | |||
Submitted,Soumis, | |||
Submitted,Validé, | |||
Tag,Étiquette, | |||
Template,Modèle, | |||
Thursday,Jeudi, | |||
@@ -4146,7 +4146,7 @@ Collapse,Réduire, | |||
"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».", | |||
{0} is not a valid Name,{0} n'est pas un nom valide, | |||
Your system is being updated. Please refresh again after a few moments.,Votre système est en cours de mise à jour. Veuillez actualiser à nouveau après quelques instants., | |||
{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.,{0} {1}: l'enregistrement soumis ne peut pas être supprimé. Vous devez d'abord {2} l'annuler {3}., | |||
{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.,{0} {1}: l'enregistrement validé ne peut pas être supprimé. Vous devez d'abord {2} l'annuler {3}., | |||
Invalid naming series (. missing) for {0},Série de noms non valide (. Manquante) pour {0}, | |||
Error has occurred in {0},Une erreur s'est produite dans {0}, | |||
Status Updated,Statut mis à jour, | |||
@@ -4510,7 +4510,7 @@ Oops,Oups, | |||
Skip Step,Passer l'étape, | |||
"You're doing great, let's take you back to the onboarding page.","Vous vous débrouillez très bien, revenons à la page d'intégration.", | |||
Good Work 🎉,Bon travail 🎉, | |||
Submit this document to complete this step.,Soumettez ce document pour terminer cette étape., | |||
Submit this document to complete this step.,Validez ce document pour terminer cette étape., | |||
Great,Génial, | |||
You may continue with onboarding,Vous pouvez continuer avec l'intégration, | |||
You seem good to go!,Vous semblez prêt à partir!, | |||
@@ -66,7 +66,7 @@ | |||
{% endif %} | |||
{% if not disable_comments %} | |||
<div class="my-5 blog-comments"> | |||
<div class="blog-comments"> | |||
{% include 'templates/includes/comments/comments.html' %} | |||
</div> | |||
{% endif %} | |||
@@ -8,8 +8,8 @@ | |||
<div class="col-md-8"> | |||
<div class="hero"> | |||
<div class="hero-content"> | |||
<h1 class="hero-title">{{ blog_title or _('Blog') }}</h1> | |||
<p class="hero-subtitle mb-0">{{ blog_introduction or '' }}</p> | |||
<h1>{{ blog_title or _('Blog') }}</h1> | |||
<p>{{ blog_introduction or '' }}</p> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -117,6 +117,34 @@ class TestBlogPost(unittest.TestCase): | |||
frappe.flags.force_website_cache = True | |||
def test_spam_comments(self): | |||
# Make a temporary Blog Post (and a Blog Category) | |||
blog = make_test_blog('Test Spam Comment') | |||
# Create a spam comment | |||
frappe.get_doc( | |||
doctype="Comment", | |||
comment_type="Comment", | |||
reference_doctype="Blog Post", | |||
reference_name=blog.name, | |||
comment_email="<a href=\"https://example.com/spam/\">spam</a>", | |||
comment_by="<a href=\"https://example.com/spam/\">spam</a>", | |||
published=1, | |||
content="More spam content. <a href=\"https://example.com/spam/\">spam</a> with link.", | |||
).insert() | |||
# Visit the blog post page | |||
set_request(path=blog.route) | |||
blog_page_response = get_response() | |||
blog_page_html = frappe.safe_decode(blog_page_response.get_data()) | |||
self.assertNotIn('<a href="https://example.com/spam/">spam</a>', blog_page_html) | |||
self.assertIn("More spam content. spam with link.", blog_page_html) | |||
# Cleanup | |||
frappe.delete_doc("Blog Post", blog.name) | |||
frappe.delete_doc("Blog Category", blog.blog_category) | |||
def scrub(text): | |||
return WebsiteGenerator.scrub(None, text) | |||
@@ -22,4 +22,5 @@ | |||
<div class="confetti confetti-2"></div> | |||
<div class="confetti confetti-3"></div> | |||
{%- endif -%} | |||
{% if cta_url %}<a href="{{ cta_url }}" class="stretched-link"></a>{% endif %} | |||
</div> |
@@ -6,7 +6,8 @@ | |||
<p class="section-description">{{ subtitle }}</p> | |||
{%- endif -%} | |||
<div class="section-features" data-columns="{{ columns or 3 }}"> | |||
<div class="section-features" data-columns="{{ columns or 3 }}" | |||
{% if not subtitle %}style="margin-top: -1.5rem"{% endif %}> | |||
{%- for feature in features -%} | |||
<div class="section-feature"> | |||
<div> | |||
@@ -16,4 +16,5 @@ | |||
{%- endif -%} | |||
</div> | |||
</div> | |||
{% if cta_url %}<a href="{{ cta_url }}" class="stretched-link"></a>{% endif %} | |||
</div> |
@@ -13,7 +13,7 @@ | |||
<img class="video-thumbnail" src="https://i.ytimg.com/vi/{{ video.youtube_id }}/sddefault.jpg"> | |||
</div> | |||
{%- if video.title -%} | |||
<h3 class="feature-title">{{ video.title }}</h3> | |||
<h4 class="feature-title">{{ video.title }}</h4> | |||
{%- endif -%} | |||
{%- if video.content -%} | |||
<p class="feature-content">{{ video.content }}</p> | |||
@@ -44,7 +44,7 @@ | |||
"js-sha256": "^0.9.0", | |||
"jsbarcode": "^3.9.0", | |||
"localforage": "^1.9.0", | |||
"moment": "^2.20.1", | |||
"moment": "^2.29.2", | |||
"moment-timezone": "^0.5.28", | |||
"node-sass": "^7.0.0", | |||
"plyr": "^3.6.2", | |||
@@ -2998,10 +2998,10 @@ moment-timezone@^0.5.28: | |||
dependencies: | |||
moment ">= 2.9.0" | |||
"moment@>= 2.9.0", moment@^2.20.1: | |||
version "2.24.0" | |||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" | |||
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== | |||
"moment@>= 2.9.0", moment@^2.29.2: | |||
version "2.29.2" | |||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" | |||
integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== | |||
ms@2.0.0: | |||
version "2.0.0" | |||