@@ -1,9 +1,13 @@ | |||
codecov: | |||
require_ci_to_pass: yes | |||
coverage: | |||
status: | |||
project: | |||
default: | |||
target: auto | |||
threshold: 0.5% | |||
comment: | |||
layout: "diff, flags, files" | |||
layout: "diff" | |||
require_changes: true |
@@ -31,8 +31,13 @@ context('API Resources', () => { | |||
}); | |||
it('Removes the Comments', () => { | |||
cy.get_list('Comment').then(body => body.data.forEach(comment => { | |||
cy.remove_doc('Comment', comment.name); | |||
})); | |||
cy.get_list('Comment').then(body => { | |||
let comment_names = []; | |||
body.data.map(comment => comment_names.push(comment.name)); | |||
comment_names = [...new Set(comment_names)]; // remove duplicates | |||
comment_names.forEach((comment_name) => { | |||
cy.remove_doc('Comment', comment_name); | |||
}); | |||
}); | |||
}); | |||
}); |
@@ -0,0 +1,93 @@ | |||
context("Control Float", () => { | |||
before(() => { | |||
cy.login(); | |||
cy.visit("/app/website"); | |||
}); | |||
function get_dialog_with_float() { | |||
return cy.dialog({ | |||
title: "Float Check", | |||
fields: [ | |||
{ | |||
fieldname: "float_number", | |||
fieldtype: "Float", | |||
Label: "Float" | |||
} | |||
] | |||
}); | |||
} | |||
it("check value changes", () => { | |||
get_dialog_with_float().as("dialog"); | |||
let data = get_data(); | |||
data.forEach(x => { | |||
cy.window() | |||
.its("frappe") | |||
.then(frappe => { | |||
frappe.boot.sysdefaults.number_format = x.number_format; | |||
}); | |||
x.values.forEach(d => { | |||
cy.get_field("float_number", "Float").clear(); | |||
cy.fill_field("float_number", d.input, "Float").blur(); | |||
cy.get_field("float_number", "Float").should( | |||
"have.value", | |||
d.blur_expected | |||
); | |||
cy.get_field("float_number", "Float").focus(); | |||
cy.get_field("float_number", "Float").blur(); | |||
cy.get_field("float_number", "Float").focus(); | |||
cy.get_field("float_number", "Float").should( | |||
"have.value", | |||
d.focus_expected | |||
); | |||
}); | |||
}); | |||
}); | |||
function get_data() { | |||
return [ | |||
{ | |||
number_format: "#.###,##", | |||
values: [ | |||
{ | |||
input: "364.87,334", | |||
blur_expected: "36.487,334", | |||
focus_expected: "36487.334" | |||
}, | |||
{ | |||
input: "36487,334", | |||
blur_expected: "36.487,334", | |||
focus_expected: "36487.334" | |||
}, | |||
{ | |||
input: "100", | |||
blur_expected: "100,000", | |||
focus_expected: "100" | |||
} | |||
] | |||
}, | |||
{ | |||
number_format: "#,###.##", | |||
values: [ | |||
{ | |||
input: "364,87.334", | |||
blur_expected: "36,487.334", | |||
focus_expected: "36487.334" | |||
}, | |||
{ | |||
input: "36487.334", | |||
blur_expected: "36,487.334", | |||
focus_expected: "36487.334" | |||
}, | |||
{ | |||
input: "100", | |||
blur_expected: "100.000", | |||
focus_expected: "100" | |||
} | |||
] | |||
} | |||
]; | |||
} | |||
}); |
@@ -1,19 +1,19 @@ | |||
context('Datetime Field Validation', () => { | |||
before(() => { | |||
cy.login(); | |||
cy.visit('/app/communication'); | |||
cy.window().its('frappe').then(frappe => { | |||
frappe.call("frappe.tests.ui_test_helpers.create_communication_records"); | |||
}); | |||
}); | |||
// TODO: Enable this again | |||
// currently this is flaky possibly because of different timezone in CI | |||
// validating datetime field value when value is set from backend and get validated on form load. | |||
it('datetime field form validation', () => { | |||
cy.visit('/app/communication'); | |||
cy.get('a[title="Test Form Communication 1"]').invoke('attr', 'data-name') | |||
.then((name) => { | |||
cy.visit(`/app/communication/${name}`); | |||
cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red'); | |||
}); | |||
}); | |||
}); | |||
// context('Datetime Field Validation', () => { | |||
// before(() => { | |||
// cy.login(); | |||
// cy.visit('/app/communication'); | |||
// }); | |||
// it('datetime field form validation', () => { | |||
// // validating datetime field value when value is set from backend and get validated on form load. | |||
// cy.window().its('frappe').then(frappe => { | |||
// return frappe.xcall("frappe.tests.ui_test_helpers.create_communication_record"); | |||
// }).then(doc => { | |||
// cy.visit(`/app/communication/${doc.name}`); | |||
// cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red'); | |||
// }); | |||
// }); | |||
// }); |
@@ -7,11 +7,11 @@ context('List View', () => { | |||
}); | |||
}); | |||
it('enables "Actions" button', () => { | |||
const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; | |||
const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; | |||
cy.go_to_list('ToDo'); | |||
cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true }); | |||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); | |||
cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 8).each((el, index) => { | |||
cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 9).each((el, index) => { | |||
cy.wrap(el).contains(actions[index]); | |||
}).then((elements) => { | |||
cy.intercept({ | |||
@@ -6,12 +6,12 @@ context('Sidebar', () => { | |||
}); | |||
it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => { | |||
cy.click_sidebar_button(0); | |||
cy.click_sidebar_button("Assigned To"); | |||
//To check if no filter is available in "Assigned To" dropdown | |||
cy.get('.empty-state').should('contain', 'No filters found'); | |||
cy.click_sidebar_button(1); | |||
cy.click_sidebar_button("Created By"); | |||
//To check if "Created By" dropdown contains filter | |||
cy.get('.group-by-item > .dropdown-item').should('contain', 'Me'); | |||
@@ -22,7 +22,7 @@ context('Sidebar', () => { | |||
cy.get_field('assign_to_me', 'Check').click(); | |||
cy.get('.modal-footer > .standard-actions > .btn-primary').click(); | |||
cy.visit('/app/doctype'); | |||
cy.click_sidebar_button(0); | |||
cy.click_sidebar_button("Assigned To"); | |||
//To check if filter is added in "Assigned To" dropdown after assignment | |||
cy.get('.group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item').should('contain', '1'); | |||
@@ -38,20 +38,19 @@ context('Sidebar', () => { | |||
cy.get('.fieldname-select-area > .awesomplete > .form-control').should('have.value', 'Assigned To'); | |||
cy.get('.condition').should('have.value', 'like'); | |||
cy.get('.filter-field > .form-group > .input-with-feedback').should('have.value', '%Administrator%'); | |||
cy.click_filter_button(); | |||
//To remove the applied filter | |||
cy.get('.filter-action-buttons > div > .btn-secondary').contains('Clear Filters').click(); | |||
cy.click_filter_button(); | |||
cy.get('.filter-selector > .btn').should('contain', 'Filter'); | |||
cy.clear_filters(); | |||
//To remove the assignment | |||
cy.visit('/app/doctype'); | |||
cy.click_listview_row_item(0); | |||
cy.get('.assignments > .avatar-group > .avatar > .avatar-frame').click(); | |||
cy.get('.remove-btn').click({force: true}); | |||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-header > .modal-actions > .btn-modal-close').click(); | |||
cy.hide_dialog(); | |||
cy.visit('/app/doctype'); | |||
cy.click_sidebar_button(0); | |||
cy.click_sidebar_button("Assigned To"); | |||
cy.get('.empty-state').should('contain', 'No filters found'); | |||
}); | |||
}); |
@@ -4,11 +4,11 @@ context('Timeline', () => { | |||
before(() => { | |||
cy.visit('/login'); | |||
cy.login(); | |||
cy.visit('/app/todo'); | |||
}); | |||
it('Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo', () => { | |||
//Adding new ToDo | |||
cy.visit('/app/todo'); | |||
cy.click_listview_primary_button('Add ToDo'); | |||
cy.findByRole('button', {name: 'Edit in full page'}).click(); | |||
cy.get('[data-fieldname="description"] .ql-editor').eq(0).type('Test ToDo', {force: true}); | |||
@@ -28,15 +28,15 @@ context('Timeline', () => { | |||
cy.get('.timeline-content').should('contain', 'Testing Timeline'); | |||
//Editing comment | |||
cy.click_timeline_action_btn(0); | |||
cy.click_timeline_action_btn("Edit"); | |||
cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(' 123'); | |||
cy.click_timeline_action_btn(0); | |||
cy.click_timeline_action_btn("Save"); | |||
//To check if the edited comment text is visible in timeline content | |||
cy.get('.timeline-content').should('contain', 'Testing Timeline 123'); | |||
//Discarding comment | |||
cy.click_timeline_action_btn(0); | |||
cy.click_timeline_action_btn("Edit"); | |||
cy.findByRole('button', {name: 'Dismiss'}).click(); | |||
//To check if after discarding the timeline content is same as previous | |||
@@ -81,7 +81,7 @@ context('Timeline', () => { | |||
cy.visit('/app/custom-submittable-doctype'); | |||
cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click(); | |||
cy.findByRole('button', {name: 'Actions'}).click(); | |||
cy.get('.actions-btn-group > .dropdown-menu > li > .grey-link').eq(7).click(); | |||
cy.get('.actions-btn-group > .dropdown-menu > li > .dropdown-item').contains("Delete").click(); | |||
cy.click_modal_primary_button('Yes', {force: true, delay: 700}); | |||
//Deleting the custom doctype | |||
@@ -187,7 +187,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { | |||
if (fieldtype === 'Select') { | |||
cy.get('@input').select(value); | |||
} else { | |||
cy.get('@input').type(value, {waitForAnimations: false, force: true}); | |||
cy.get('@input').type(value, {waitForAnimations: false, force: true, delay: 100}); | |||
} | |||
return cy.get('@input'); | |||
}); | |||
@@ -252,7 +252,8 @@ Cypress.Commands.add('new_form', doctype => { | |||
}); | |||
Cypress.Commands.add('go_to_list', doctype => { | |||
cy.visit(`/app/list/${doctype}/list`); | |||
let dt_in_route = doctype.toLowerCase().replace(/ /g, '-'); | |||
cy.visit(`/app/${dt_in_route}`); | |||
}); | |||
Cypress.Commands.add('clear_cache', () => { | |||
@@ -316,7 +317,11 @@ Cypress.Commands.add('add_filter', () => { | |||
}); | |||
Cypress.Commands.add('clear_filters', () => { | |||
cy.get('.filter-section .filter-button').click(); | |||
cy.intercept({ | |||
method: 'POST', | |||
url: 'api/method/frappe.model.utils.user_settings.save' | |||
}).as('filter-saved'); | |||
cy.get('.filter-section .filter-button').click({force: true}); | |||
cy.wait(300); | |||
cy.get('.filter-popover').should('exist'); | |||
cy.get('.filter-popover').find('.clear-filters').click(); | |||
@@ -324,16 +329,15 @@ Cypress.Commands.add('clear_filters', () => { | |||
cy.window().its('cur_list').then(cur_list => { | |||
cur_list && cur_list.filter_area && cur_list.filter_area.clear(); | |||
}); | |||
cy.wait('@filter-saved'); | |||
}); | |||
Cypress.Commands.add('click_modal_primary_button', (btn_name) => { | |||
cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).trigger('click', {force: true}); | |||
}); | |||
Cypress.Commands.add('click_sidebar_button', (btn_no) => { | |||
cy.get('.list-group-by-fields > .group-by-field > .btn').eq(btn_no).click(); | |||
Cypress.Commands.add('click_sidebar_button', (btn_name) => { | |||
cy.get('.list-group-by-fields .list-link > a').contains(btn_name).click({force: true}); | |||
}); | |||
Cypress.Commands.add('click_listview_row_item', (row_no) => { | |||
@@ -348,6 +352,6 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => { | |||
cy.get('.primary-action').contains(btn_name).click({force: true}); | |||
}); | |||
Cypress.Commands.add('click_timeline_action_btn', (btn_no) => { | |||
cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').eq(btn_no).first().click(); | |||
Cypress.Commands.add('click_timeline_action_btn', (btn_name) => { | |||
cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').contains(btn_name).click(); | |||
}); |
@@ -618,8 +618,6 @@ def read_only(): | |||
try: | |||
retval = fn(*args, **get_newargs(fn, kwargs)) | |||
except: | |||
raise | |||
finally: | |||
if local and hasattr(local, 'primary_db'): | |||
local.db.close() | |||
@@ -629,6 +627,29 @@ def read_only(): | |||
return wrapper_fn | |||
return innfn | |||
def write_only(): | |||
# if replica connection exists, we have to replace it momentarily with the primary connection | |||
def innfn(fn): | |||
def wrapper_fn(*args, **kwargs): | |||
primary_db = getattr(local, "primary_db", None) | |||
replica_db = getattr(local, "replica_db", None) | |||
in_read_only = getattr(local, "db", None) != primary_db | |||
# switch to primary connection | |||
if in_read_only and primary_db: | |||
local.db = local.primary_db | |||
try: | |||
retval = fn(*args, **get_newargs(fn, kwargs)) | |||
finally: | |||
# switch back to replica connection | |||
if in_read_only and replica_db: | |||
local.db = replica_db | |||
return retval | |||
return wrapper_fn | |||
return innfn | |||
def only_for(roles, message=False): | |||
"""Raise `frappe.PermissionError` if the user does not have any of the given **Roles**. | |||
@@ -65,7 +65,7 @@ class Address(Document): | |||
def has_link(self, doctype, name): | |||
for link in self.links: | |||
if link.link_doctype==doctype and link.link_name== name: | |||
if link.link_doctype == doctype and link.link_name == name: | |||
return True | |||
def has_common_link(self, doc): | |||
@@ -47,14 +47,14 @@ class Contact(Document): | |||
def get_link_for(self, link_doctype): | |||
'''Return the link name, if exists for the given link DocType''' | |||
for link in self.links: | |||
if link.link_doctype==link_doctype: | |||
if link.link_doctype == link_doctype: | |||
return link.link_name | |||
return None | |||
def has_link(self, doctype, name): | |||
for link in self.links: | |||
if link.link_doctype==doctype and link.link_name== name: | |||
if link.link_doctype == doctype and link.link_name == name: | |||
return True | |||
def has_common_link(self, doc): | |||
@@ -9,6 +9,7 @@ class AccessLog(Document): | |||
@frappe.whitelist() | |||
@frappe.write_only() | |||
def make_access_log(doctype=None, document=None, method=None, file_type=None, | |||
report_name=None, filters=None, page=None, columns=None): | |||
@@ -36,8 +36,11 @@ class UserType(Document): | |||
if not self.user_doctypes: | |||
return | |||
modules = frappe.get_all('DocType', fields=['distinct module as module'], | |||
filters={'name': ('in', [d.document_type for d in self.user_doctypes])}) | |||
modules = frappe.get_all("DocType", | |||
fields=["module"], | |||
filters={"name": ("in", [d.document_type for d in self.user_doctypes])}, | |||
distinct=True, | |||
) | |||
self.set('user_type_modules', []) | |||
for row in modules: | |||
@@ -1,21 +0,0 @@ | |||
.version-info { | |||
overflow: auto; | |||
} | |||
.version-info pre { | |||
border: 0px; | |||
margin: 0px; | |||
background-color: inherit; | |||
} | |||
.version-info .table { | |||
background-color: inherit; | |||
} | |||
.version-info .success { | |||
background-color: #dff0d8 !important; | |||
} | |||
.version-info .danger { | |||
background-color: #f2dede !important; | |||
} |
@@ -256,11 +256,11 @@ class MariaDBDatabase(Database): | |||
index_name=index_name | |||
)) | |||
def add_index(self, doctype, fields, index_name=None): | |||
def add_index(self, doctype: str, fields: List, index_name: str = None): | |||
"""Creates an index with given fields if not already created. | |||
Index name will be `fieldname1_fieldname2_index`""" | |||
index_name = index_name or self.get_index_name(fields) | |||
table_name = 'tab' + doctype | |||
table_name = get_table_name(doctype) | |||
if not self.has_index(table_name, index_name): | |||
self.commit() | |||
self.sql("""ALTER TABLE `%s` | |||
@@ -258,14 +258,14 @@ class PostgresDatabase(Database): | |||
return self.sql("""SELECT 1 FROM pg_indexes WHERE tablename='{table_name}' | |||
and indexname='{index_name}' limit 1""".format(table_name=table_name, index_name=index_name)) | |||
def add_index(self, doctype, fields, index_name=None): | |||
def add_index(self, doctype: str, fields: List, index_name: str = None): | |||
"""Creates an index with given fields if not already created. | |||
Index name will be `fieldname1_fieldname2_index`""" | |||
table_name = get_table_name(doctype) | |||
index_name = index_name or self.get_index_name(fields) | |||
table_name = 'tab' + doctype | |||
fields_str = '", "'.join(re.sub(r"\(.*\)", "", field) for field in fields) | |||
self.commit() | |||
self.sql("""CREATE INDEX IF NOT EXISTS "{}" ON `{}`("{}")""".format(index_name, table_name, '", "'.join(fields))) | |||
self.sql_ddl(f'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}` ("{fields_str}")') | |||
def add_unique(self, doctype, fields, constraint_name=None): | |||
if isinstance(fields, str): | |||
@@ -20,5 +20,46 @@ frappe.ui.form.on('System Console', { | |||
$btn.text(__('Execute')); | |||
}); | |||
}); | |||
}, | |||
show_processlist: function(frm) { | |||
if (frm.doc.show_processlist) { | |||
// keep refreshing every 5 seconds | |||
frm.events.refresh_processlist(frm); | |||
frm.processlist_interval = setInterval(() => frm.events.refresh_processlist(frm), 5000); | |||
} else { | |||
if (frm.processlist_interval) { | |||
// end it | |||
clearInterval(frm.processlist_interval); | |||
} | |||
} | |||
}, | |||
refresh_processlist: function(frm) { | |||
let timestamp = new Date(); | |||
frappe.call('frappe.desk.doctype.system_console.system_console.show_processlist').then(r => { | |||
let rows = ''; | |||
for (let row of r.message) { | |||
rows += `<tr> | |||
<td>${row.Id}</td> | |||
<td>${row.Time}</td> | |||
<td>${row.State}</td> | |||
<td>${row.Info}</td> | |||
<td>${row.Progress}</td> | |||
</tr>` | |||
} | |||
frm.get_field('processlist').html(` | |||
<p class='text-muted'>Requested on: ${timestamp}</p> | |||
<table class='table-bordered' style='width: 100%'> | |||
<thead><tr> | |||
<th width='10%'>Id</ht> | |||
<th width='10%'>Time</ht> | |||
<th width='10%'>State</ht> | |||
<th width='60%'>Info</ht> | |||
<th width='10%'>Progress</ht> | |||
</tr></thead> | |||
<tbody>${rows}</thead>`); | |||
}); | |||
} | |||
}); |
@@ -17,9 +17,13 @@ | |||
"editable_grid": 1, | |||
"engine": "InnoDB", | |||
"field_order": [ | |||
"execute_section", | |||
"console", | |||
"commit", | |||
"output" | |||
"output", | |||
"database_processes_section", | |||
"show_processlist", | |||
"processlist" | |||
], | |||
"fields": [ | |||
{ | |||
@@ -40,13 +44,34 @@ | |||
"fieldname": "commit", | |||
"fieldtype": "Check", | |||
"label": "Commit" | |||
}, | |||
{ | |||
"fieldname": "execute_section", | |||
"fieldtype": "Section Break", | |||
"label": "Execute" | |||
}, | |||
{ | |||
"fieldname": "database_processes_section", | |||
"fieldtype": "Section Break", | |||
"label": "Database Processes" | |||
}, | |||
{ | |||
"default": "0", | |||
"fieldname": "show_processlist", | |||
"fieldtype": "Check", | |||
"label": "Show Processlist" | |||
}, | |||
{ | |||
"fieldname": "processlist", | |||
"fieldtype": "HTML", | |||
"label": "processlist" | |||
} | |||
], | |||
"hide_toolbar": 1, | |||
"index_web_pages_for_search": 1, | |||
"issingle": 1, | |||
"links": [], | |||
"modified": "2020-08-21 14:44:35.296877", | |||
"modified": "2021-09-09 13:10:14.237113", | |||
"modified_by": "Administrator", | |||
"module": "Desk", | |||
"name": "System Console", | |||
@@ -65,4 +90,4 @@ | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"track_changes": 1 | |||
} | |||
} |
@@ -33,4 +33,9 @@ class SystemConsole(Document): | |||
def execute_code(doc): | |||
console = frappe.get_doc(json.loads(doc)) | |||
console.run() | |||
return console.as_dict() | |||
return console.as_dict() | |||
@frappe.whitelist() | |||
def show_processlist(): | |||
frappe.only_for('System Manager') | |||
return frappe.db.sql('show full processlist', as_dict=1) |
@@ -164,7 +164,8 @@ doc_events = { | |||
"after_rename": "frappe.desk.notifications.clear_doctype_notifications", | |||
"on_cancel": [ | |||
"frappe.desk.notifications.clear_doctype_notifications", | |||
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions" | |||
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", | |||
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers" | |||
], | |||
"on_trash": [ | |||
"frappe.desk.notifications.clear_doctype_notifications", | |||
@@ -1,4 +1,17 @@ | |||
frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlInt { | |||
make_input() { | |||
super.make_input(); | |||
const change_handler = e => { | |||
if (this.change) this.change(e); | |||
else { | |||
let value = this.get_input_value(); | |||
this.parse_validate_and_set_in_model(value, e); | |||
} | |||
}; | |||
// convert to number format on focusout since focus converts it to flt. | |||
this.$input.on("focusout", change_handler); | |||
} | |||
parse(value) { | |||
value = this.eval_expression(value); | |||
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision()); | |||
@@ -140,7 +140,7 @@ frappe.msgprint = function(msg, title, is_minimizable) { | |||
return; | |||
} | |||
if(data.alert) { | |||
if(data.alert || data.toast) { | |||
frappe.show_alert(data); | |||
return; | |||
} | |||
@@ -361,7 +361,7 @@ frappe.hide_progress = function() { | |||
} | |||
// Floating Message | |||
frappe.show_alert = function(message, seconds=7, actions={}) { | |||
frappe.show_alert = frappe.toast = function(message, seconds=7, actions={}) { | |||
let indicator_icon_map = { | |||
'orange': "solid-warning", | |||
'yellow': "solid-warning", | |||
@@ -8,7 +8,12 @@ if (!window.frappe) window.frappe = {}; | |||
function flt(v, decimals, number_format) { | |||
if (v == null || v == '') return 0; | |||
if (typeof v !== "number") { | |||
if (!(typeof v === "number" || String(parseFloat(v)) == v)) { | |||
// cases in which this block should not run | |||
// 1. 'v' is already a number | |||
// 2. v is already parsed but in string form | |||
// if (typeof v !== "number") { | |||
v = v + ""; | |||
// strip currency symbol if exists | |||
@@ -25,6 +30,7 @@ function flt(v, decimals, number_format) { | |||
v = 0; | |||
} | |||
v = parseFloat(v); | |||
if (decimals != null) | |||
return _round(v, decimals); | |||
return v; | |||
@@ -47,3 +47,4 @@ | |||
@import "link_preview"; | |||
@import "../common/quill"; | |||
@import "plyr"; | |||
@import "version"; |
@@ -0,0 +1,33 @@ | |||
.version-info { | |||
overflow: auto; | |||
pre { | |||
border: 0px; | |||
margin: 0px; | |||
background-color: inherit; | |||
} | |||
.table { | |||
background-color: inherit; | |||
} | |||
.success { | |||
background-color: var(--green-100) !important; | |||
} | |||
.danger { | |||
background-color: var(--red-100) !important; | |||
} | |||
} | |||
[data-theme="dark"] { | |||
.version-info { | |||
.danger, .success { | |||
color: var(--gray-900); | |||
td { | |||
color: var(--gray-900); | |||
} | |||
} | |||
} | |||
} |
@@ -8,6 +8,7 @@ | |||
@import "../common/flex"; | |||
@import "../common/buttons"; | |||
@import "../common/modal"; | |||
@import "../desk/toast"; | |||
@import "../common/indicator"; | |||
@import "../common/controls"; | |||
@import "../common/awesomeplete"; | |||
@@ -197,6 +197,7 @@ class TestDB(unittest.TestCase): | |||
frappe.delete_doc(test_doctype, doc) | |||
clear_custom_fields(test_doctype) | |||
@run_only_if(db_type_is.MARIADB) | |||
class TestDDLCommandsMaria(unittest.TestCase): | |||
test_table_name = "TestNotes" | |||
@@ -205,7 +206,7 @@ class TestDDLCommandsMaria(unittest.TestCase): | |||
frappe.db.commit() | |||
frappe.db.sql( | |||
f""" | |||
CREATE TABLE `tab{self.test_table_name}` (`id` INT NULL,PRIMARY KEY (`id`)); | |||
CREATE TABLE `tab{self.test_table_name}` (`id` INT NULL, content TEXT, PRIMARY KEY (`id`)); | |||
""" | |||
) | |||
@@ -230,7 +231,10 @@ class TestDDLCommandsMaria(unittest.TestCase): | |||
def test_describe(self) -> None: | |||
self.assertEqual( | |||
(("id", "int(11)", "NO", "PRI", None, ""),), | |||
( | |||
("id", "int(11)", "NO", "PRI", None, ""), | |||
("content", "text", "YES", "", None, ""), | |||
), | |||
frappe.db.describe(self.test_table_name), | |||
) | |||
@@ -240,6 +244,17 @@ class TestDDLCommandsMaria(unittest.TestCase): | |||
self.assertGreater(len(test_table_description), 0) | |||
self.assertIn("varchar(255)", test_table_description[0]) | |||
def test_add_index(self) -> None: | |||
index_name = "test_index" | |||
frappe.db.add_index(self.test_table_name, ["id", "content(50)"], index_name) | |||
indexs_in_table = frappe.db.sql( | |||
f""" | |||
SHOW INDEX FROM tab{self.test_table_name} | |||
WHERE Key_name = '{index_name}'; | |||
""" | |||
) | |||
self.assertEquals(len(indexs_in_table), 2) | |||
@run_only_if(db_type_is.POSTGRES) | |||
class TestDDLCommandsPost(unittest.TestCase): | |||
@@ -248,7 +263,7 @@ class TestDDLCommandsPost(unittest.TestCase): | |||
def setUp(self) -> None: | |||
frappe.db.sql( | |||
f""" | |||
CREATE TABLE "tab{self.test_table_name}" ("id" INT NULL,PRIMARY KEY ("id")) | |||
CREATE TABLE "tab{self.test_table_name}" ("id" INT NULL, content text, PRIMARY KEY ("id")) | |||
""" | |||
) | |||
@@ -273,7 +288,9 @@ class TestDDLCommandsPost(unittest.TestCase): | |||
self.test_table_name = new_table_name | |||
def test_describe(self) -> None: | |||
self.assertEqual([("id",)], frappe.db.describe(self.test_table_name)) | |||
self.assertEqual( | |||
[("id",), ("content",)], frappe.db.describe(self.test_table_name) | |||
) | |||
def test_change_type(self) -> None: | |||
frappe.db.change_column_type(self.test_table_name, "id", "varchar(255)") | |||
@@ -292,3 +309,15 @@ class TestDDLCommandsPost(unittest.TestCase): | |||
self.assertGreater(len(check_change), 0) | |||
self.assertIn("character varying", check_change[0]) | |||
def test_add_index(self) -> None: | |||
index_name = "test_index" | |||
frappe.db.add_index(self.test_table_name, ["id", "content(50)"], index_name) | |||
indexs_in_table = frappe.db.sql( | |||
f""" | |||
SELECT indexname | |||
FROM pg_indexes | |||
WHERE tablename = 'tab{self.test_table_name}' | |||
AND indexname = '{index_name}' ; | |||
""", | |||
) | |||
self.assertEquals(len(indexs_in_table), 1) |
@@ -63,11 +63,12 @@ class TestTranslate(unittest.TestCase): | |||
Case 2: frappe.form_dict._lang is not set, but preferred_language cookie is | |||
""" | |||
with patch.object(frappe.translate, "get_preferred_language_cookie", return_value=second_lang): | |||
set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)]) | |||
with patch.object(frappe.translate, "get_preferred_language_cookie", return_value='fr'): | |||
set_request(method="POST", path="/", headers=[("Accept-Language", 'hr')]) | |||
return_val = get_language() | |||
self.assertNotIn(return_val, [second_lang, get_parent_language(second_lang)]) | |||
# system default language | |||
self.assertEqual(return_val, 'en') | |||
self.assertNotIn(return_val, [second_lang, get_parent_language(second_lang)]) | |||
def test_guest_request_language_resolution_with_cookie(self): | |||
"""Test for frappe.translate.get_language | |||
@@ -62,16 +62,15 @@ def create_todo_records(): | |||
}).insert() | |||
@frappe.whitelist() | |||
def create_communication_records(): | |||
if frappe.db.get_all('Communication', {'subject': 'Test Form Communication 1'}): | |||
return | |||
frappe.get_doc({ | |||
def create_communication_record(): | |||
doc = frappe.get_doc({ | |||
"doctype": "Communication", | |||
"recipients": "test@gmail.com", | |||
"subject": "Test Form Communication 1", | |||
"communication_date": frappe.utils.now_datetime(), | |||
}).insert() | |||
}) | |||
doc.insert() | |||
return doc | |||
@frappe.whitelist() | |||
def setup_workflow(): | |||
@@ -3262,7 +3262,7 @@ Drop,drop, | |||
Drop Here,Drop hier, | |||
Drop files here,Laat lêers hier neer, | |||
Dynamic Template,Dinamiese sjabloon, | |||
ERPNext Role,ERPVolgende rol, | |||
ERPNext Role,ERPNext rol, | |||
Email / Notifications,E-pos / kennisgewings, | |||
Email Account setup please enter your password for: {0},Voer u wagwoord in vir die e-posrekening vir: {0}, | |||
Email Address whose Google Contacts are to be synced.,E-posadres waarvan die Google-kontakte gesinkroniseer moet word., | |||
@@ -3262,7 +3262,7 @@ Drop,ጣል ያድርጉ።, | |||
Drop Here,እዚህ ጣል ያድርጉ።, | |||
Drop files here,ፋይሎችን እዚህ ይጣሉ።, | |||
Dynamic Template,ተለዋዋጭ አብነት, | |||
ERPNext Role,የኢአርኤክስ ቀጣይ ሚና።, | |||
ERPNext Role,ERPNext ሚና, | |||
Email / Notifications,ኢሜይል / ማስታወቂያዎች, | |||
Email Account setup please enter your password for: {0},የኢሜል አካውንት ማዋቀር እባክዎን ለሚከተለው ይለፍ ቃልዎን ያስገቡ ፦ {0}, | |||
Email Address whose Google Contacts are to be synced.,የጉግል አድራሻዎች የሚመሳሰሉበት የኢሜል አድራሻ ፡፡, | |||
@@ -3262,7 +3262,7 @@ Drop,Dråbe, | |||
Drop Here,Drop Here, | |||
Drop files here,Slip filer her, | |||
Dynamic Template,Dynamisk skabelon, | |||
ERPNext Role,ERPNæste rolle, | |||
ERPNext Role,ERPNext rolle, | |||
Email / Notifications,E-mail / underretninger, | |||
Email Account setup please enter your password for: {0},Opsætning af e-mail-konto: indtast venligst din adgangskode til: {0}, | |||
Email Address whose Google Contacts are to be synced.,"E-mail-adresse, hvis Google-kontakter skal synkroniseres.", | |||
@@ -3262,7 +3262,7 @@ Drop,Penurunan, | |||
Drop Here,Jatuhkan Di Sini, | |||
Drop files here,Letakkan file di sini, | |||
Dynamic Template,Template Dinamis, | |||
ERPNext Role,Peran ERPN, | |||
ERPNext Role,Peran ERPNext, | |||
Email / Notifications,Notifikasi email, | |||
Email Account setup please enter your password for: {0},"Pengaturan Akun Email, harap masukkan kata sandi Anda untuk: {0}", | |||
Email Address whose Google Contacts are to be synced.,Alamat Email yang Kontak Google-nya harus disinkronkan., | |||
@@ -3262,7 +3262,7 @@ Drop,Dropi, | |||
Drop Here,Sendu hér, | |||
Drop files here,Sendu skrár hér, | |||
Dynamic Template,Dynamískt sniðmát, | |||
ERPNext Role,ERPNæsta hlutverk, | |||
ERPNext Role,ERPNext hlutverk, | |||
Email / Notifications,Netfang / tilkynningar, | |||
Email Account setup please enter your password for: {0},Uppsetning tölvupóstreikninga vinsamlegast sláðu inn lykilorðið þitt fyrir: {0}, | |||
Email Address whose Google Contacts are to be synced.,Netfang þar sem samstillt er Google tengiliði, | |||
@@ -3262,7 +3262,7 @@ Drop,Far cadere, | |||
Drop Here,Drop Here, | |||
Drop files here,Trascina i file qui, | |||
Dynamic Template,Modello dinamico, | |||
ERPNext Role,ERPSuccessivo ruolo, | |||
ERPNext Role,ruolo ERPNext, | |||
Email / Notifications,Notifiche di posta elettronica, | |||
Email Account setup please enter your password for: {0},"Impostazione dell'account e-mail, inserire la password per: {0}", | |||
Email Address whose Google Contacts are to be synced.,Indirizzo email i cui contatti Google devono essere sincronizzati., | |||
@@ -3262,7 +3262,7 @@ Drop,ドロップ, | |||
Drop Here,ここにドロップ, | |||
Drop files here,ここにファイルをドロップします, | |||
Dynamic Template,動的テンプレート, | |||
ERPNext Role,ERP次のロール, | |||
ERPNext Role,ERPNext の役割, | |||
Email / Notifications,メール/通知, | |||
Email Account setup please enter your password for: {0},メールアカウントのセットアップ:{0}のパスワードを入力してください, | |||
Email Address whose Google Contacts are to be synced.,Googleの連絡先を同期するメールアドレス。, | |||
@@ -3262,7 +3262,7 @@ Drop,ទម្លាក់។, | |||
Drop Here,ទម្លាក់នៅទីនេះ។, | |||
Drop files here,ទម្លាក់ឯកសារនៅទីនេះ។, | |||
Dynamic Template,គំរូឌីណាមិក, | |||
ERPNext Role,តួនាទី ERP បន្ទាប់។, | |||
ERPNext Role,ERPNext តួនាទី។, | |||
Email / Notifications,អ៊ីមែល / ការជូនដំណឹង, | |||
Email Account setup please enter your password for: {0},រៀបចំគណនីអ៊ីមែលសូមបញ្ចូលពាក្យសម្ងាត់របស់អ្នកសម្រាប់៖ {0}, | |||
Email Address whose Google Contacts are to be synced.,អាសយដ្ឋានអ៊ីមែលដែលទំនាក់ទំនងរបស់ Google នឹងត្រូវធ្វើសមកាលកម្ម។, | |||
@@ -3262,7 +3262,7 @@ Drop,थेंब, | |||
Drop Here,येथे ड्रॉप करा, | |||
Drop files here,फायली येथे सोडा, | |||
Dynamic Template,डायनॅमिक टेम्पलेट, | |||
ERPNext Role,ईआरपीनेक्स्ट रोल, | |||
ERPNext Role,ERPNext रोल, | |||
Email / Notifications,ईमेल / सूचना, | |||
Email Account setup please enter your password for: {0},ईमेल खाते सेटअप यासाठी आपला संकेतशब्द प्रविष्ट करा: {0}, | |||
Email Address whose Google Contacts are to be synced.,ज्यांचे Google संपर्क समक्रमित केले जातील असा ईमेल पत्ता., | |||
@@ -3262,7 +3262,7 @@ Drop,Tera, | |||
Drop Here,Tera Hano, | |||
Drop files here,Tera dosiye hano, | |||
Dynamic Template,Icyitegererezo, | |||
ERPNext Role,Uruhare rwa ERPN, | |||
ERPNext Role,uruhare rwa ERPNext, | |||
Email / Notifications,Imeri / Amatangazo, | |||
Email Account setup please enter your password for: {0},Imeri ya konte ya imeri nyamuneka andika ijambo ryibanga rya: {0}, | |||
Email Address whose Google Contacts are to be synced.,Aderesi ya imeri abo Google igomba guhuza., | |||
@@ -3262,7 +3262,7 @@ Drop,Pokles, | |||
Drop Here,Drop sem, | |||
Drop files here,Sem presuňte súbory, | |||
Dynamic Template,Dynamická šablóna, | |||
ERPNext Role,ERPĎalšia rola, | |||
ERPNext Role,ERPNext rola, | |||
Email / Notifications,E-mail / Upozornenia, | |||
Email Account setup please enter your password for: {0},"Nastavenie e-mailového účtu, zadajte heslo pre: {0}", | |||
Email Address whose Google Contacts are to be synced.,"E-mailová adresa, ktorej kontakty Google sa majú synchronizovať.", | |||
@@ -3262,7 +3262,7 @@ Drop,Drop, | |||
Drop Here,Hidh këtu, | |||
Drop files here,Hidh skedarët këtu, | |||
Dynamic Template,Modeli Dinamik, | |||
ERPNext Role,Roli ERPN, | |||
ERPNext Role,Roli i ERPNext, | |||
Email / Notifications,Email / njoftime, | |||
Email Account setup please enter your password for: {0},Konfigurimi i llogarisë email ju lutemi shkruani fjalëkalimin tuaj për: {0, | |||
Email Address whose Google Contacts are to be synced.,Adresa e Email-it Kontaktet e të cilit Google duhet të sinkronizohen., | |||
@@ -3262,7 +3262,7 @@ Drop,Släppa, | |||
Drop Here,Släpp här, | |||
Drop files here,Släpp filer här, | |||
Dynamic Template,Dynamisk mall, | |||
ERPNext Role,ERPNästa roll, | |||
ERPNext Role,ERPNext roll, | |||
Email / Notifications,E-post / aviseringar, | |||
Email Account setup please enter your password for: {0},"Ange e-postkonto, ange ditt lösenord för: {0}", | |||
Email Address whose Google Contacts are to be synced.,E-postadress vars Google-kontakter ska synkroniseras., | |||
@@ -47,7 +47,7 @@ frappe.ui.form.on("Web Form", { | |||
frm.add_custom_button(__('Get Fields'), () => { | |||
let webform_fieldtypes = frappe.meta.get_field('Web Form Field', 'fieldtype').options.split('\n'); | |||
let fieldnames = (frm.doc.fields || []).map(d => d.fieldname); | |||
let fieldnames = (frm.doc.web_form_fields || []).map(d => d.fieldname); | |||
frappe.model.with_doctype(frm.doc.doc_type, () => { | |||
let meta = frappe.get_meta(frm.doc.doc_type); | |||
for (let field of meta.fields) { | |||