Merge branch 'frappe:develop' into wiki-based-desk

This commit is contained in:
Shariq Ansari 2021-08-05 11:30:18 +05:30 committed by GitHub
commit cec1de0389
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1474 additions and 386 deletions

View file

@ -0,0 +1,50 @@
context('Control Icon', () => {
before(() => {
cy.login();
cy.visit('/app/website');
});
function get_dialog_with_icon() {
return cy.dialog({
title: 'Icon',
fields: [{
label: 'Icon',
fieldname: 'icon',
fieldtype: 'Icon'
}]
});
}
it('should set icon', () => {
get_dialog_with_icon().as('dialog');
cy.get('.frappe-control[data-fieldname=icon] input').first().click();
cy.get('.icon-picker .icon-wrapper[id=active]').first().click();
cy.get('.frappe-control[data-fieldname=icon] input').first().should('have.value', 'active');
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('icon');
expect(value).to.equal('active');
});
cy.get('.icon-picker .icon-wrapper[id=resting]').first().click();
cy.get('.frappe-control[data-fieldname=icon] input').first().should('have.value', 'resting');
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('icon');
expect(value).to.equal('resting');
});
});
it('search for icon and clear search input', () => {
let search_text = 'ed';
cy.get('.icon-picker input[type=search]').first().click().type(search_text);
cy.get('.icon-section .icon-wrapper:not(.hidden)').then(i => {
cy.get(`.icon-section .icon-wrapper[id*='${search_text}']`).then(icons => {
expect(i.length).to.equal(icons.length);
});
});
cy.get('.icon-picker input[type=search]').clear().blur();
cy.get('.icon-section .icon-wrapper').should('not.have.class', 'hidden');
});
});

View file

@ -154,7 +154,6 @@ class LoginManager:
self.make_session()
self.setup_boot_cache()
self.set_user_info()
self.clear_preferred_language()
def get_user_info(self):
self.info = frappe.db.get_value("User", self.user,

View file

@ -561,30 +561,54 @@ def move(dest_dir, site):
return final_new_path
@click.command('set-admin-password')
@click.argument('admin-password')
@click.command('set-password')
@click.argument('user')
@click.argument('password', required=False)
@click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False)
@pass_context
def set_admin_password(context, admin_password, logout_all_sessions=False):
def set_password(context, user, password=None, logout_all_sessions=False):
"Set password for a user on a site"
if not context.sites:
raise SiteNotSpecifiedError
for site in context.sites:
set_user_password(site, user, password, logout_all_sessions)
@click.command('set-admin-password')
@click.argument('admin-password', required=False)
@click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False)
@pass_context
def set_admin_password(context, admin_password=None, logout_all_sessions=False):
"Set Administrator password for a site"
if not context.sites:
raise SiteNotSpecifiedError
for site in context.sites:
set_user_password(site, "Administrator", admin_password, logout_all_sessions)
def set_user_password(site, user, password, logout_all_sessions=False):
import getpass
from frappe.utils.password import update_password
for site in context.sites:
try:
frappe.init(site=site)
try:
frappe.init(site=site)
while not admin_password:
admin_password = getpass.getpass("Administrator's password for {0}: ".format(site))
while not password:
password = getpass.getpass(f"{user}'s password for {site}: ")
frappe.connect()
if not frappe.db.exists("User", user):
print(f"User {user} does not exist")
sys.exit(1)
update_password(user=user, pwd=password, logout_all_sessions=logout_all_sessions)
frappe.db.commit()
password = None
finally:
frappe.destroy()
frappe.connect()
update_password(user='Administrator', pwd=admin_password, logout_all_sessions=logout_all_sessions)
frappe.db.commit()
admin_password = None
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@click.command('set-last-active-for-user')
@click.option('--user', help="Setup last active date for user")
@ -729,6 +753,7 @@ commands = [
remove_from_installed_apps,
restore,
run_patch,
set_password,
set_admin_password,
uninstall,
disable_user,

View file

@ -90,7 +90,7 @@
"label": "Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"reqd": 1,
"search_index": 1
},
@ -487,7 +487,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-29 06:09:26.454990",
"modified": "2021-07-10 21:56:04.167745",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -66,4 +66,92 @@ frappe.ui.form.on('DocType', {
autoname: function(frm) {
frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt');
}
})
});
frappe.ui.form.on("DocField", {
form_render(frm, doctype, docname) {
// Render two select fields for Fetch From instead of Small Text for better UX
let field = frm.cur_grid.grid_form.fields_dict.fetch_from;
$(field.input_area).hide();
let $doctype_select = $(`<select class="form-control">`);
let $field_select = $(`<select class="form-control">`);
let $wrapper = $('<div class="fetch-from-select row"><div>');
$wrapper.append($doctype_select, $field_select);
field.$input_wrapper.append($wrapper);
$doctype_select.wrap('<div class="col"></div>');
$field_select.wrap('<div class="col"></div>');
let row = frappe.get_doc(doctype, docname);
let curr_value = { doctype: null, fieldname: null };
if (row.fetch_from) {
let [doctype, fieldname] = row.fetch_from.split(".");
curr_value.doctype = doctype;
curr_value.fieldname = fieldname;
}
let curr_df_link_doctype = row.fieldtype == "Link" ? row.options : null;
let doctypes = frm.doc.fields
.filter(df => df.fieldtype == "Link")
.filter(df => df.options && df.options != curr_df_link_doctype)
.map(df => ({
label: `${df.options} (${df.fieldname})`,
value: df.fieldname
}));
$doctype_select.add_options([
{ label: __("Select DocType"), value: "", selected: true },
...doctypes
]);
$doctype_select.on("change", () => {
row.fetch_from = "";
frm.dirty();
update_fieldname_options();
});
function update_fieldname_options() {
$field_select.find("option").remove();
let link_fieldname = $doctype_select.val();
if (!link_fieldname) return;
let link_field = frm.doc.fields.find(
df => df.fieldname === link_fieldname
);
let link_doctype = link_field.options;
frappe.model.with_doctype(link_doctype, () => {
let fields = frappe.meta
.get_docfields(link_doctype, null, {
fieldtype: ["not in", frappe.model.no_value_type]
})
.map(df => ({
label: `${df.label} (${df.fieldtype})`,
value: df.fieldname
}));
$field_select.add_options([
{
label: __("Select Field"),
value: "",
selected: true,
disabled: true
},
...fields
]);
if (curr_value.fieldname) {
$field_select.val(curr_value.fieldname);
}
});
}
$field_select.on("change", () => {
let fetch_from = `${$doctype_select.val()}.${$field_select.val()}`;
row.fetch_from = fetch_from;
frm.dirty();
});
if (curr_value.doctype) {
$doctype_select.val(curr_value.doctype);
update_fieldname_options();
}
}
});

View file

@ -931,6 +931,9 @@ def validate_fields(meta):
if meta.website_search_field not in fieldname_list:
frappe.throw(_("Website Search Field must be a valid fieldname"), InvalidFieldNameError)
if "title" not in fieldname_list:
frappe.throw(_('Field "title" is mandatory if "Website Search Field" is set.'), title=_("Missing Field"))
def check_timeline_field(meta):
if not meta.timeline_field:
return

View file

@ -120,7 +120,7 @@
"label": "Field Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"reqd": 1
},
{
@ -417,7 +417,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-07-12 04:54:12.042319",
"modified": "2021-07-12 05:54:13.042319",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",

View file

@ -82,7 +82,7 @@
"label": "Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1,
"search_index": 1
},
@ -428,7 +428,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-29 06:11:57.661039",
"modified": "2021-07-10 21:57:24.479749",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -51,7 +51,8 @@ class MariaDBDatabase(Database):
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('longtext', ''),
'Geolocation': ('longtext', ''),
'Duration': ('decimal', '18,6')
'Duration': ('decimal', '18,6'),
'Icon': ('varchar', self.VARCHAR_LEN)
}
def get_connection(self):

View file

@ -60,7 +60,8 @@ class PostgresDatabase(Database):
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('text', ''),
'Geolocation': ('text', ''),
'Duration': ('decimal', '18,6')
'Duration': ('decimal', '18,6'),
'Icon': ('varchar', self.VARCHAR_LEN)
}
def get_connection(self):

View file

@ -4,7 +4,7 @@
import unittest
import frappe
from frappe.integrations.doctype.webhook.webhook import get_webhook_headers, get_webhook_data
from frappe.integrations.doctype.webhook.webhook import get_webhook_headers, get_webhook_data, enqueue_webhook
class TestWebhook(unittest.TestCase):
@ -12,6 +12,8 @@ class TestWebhook(unittest.TestCase):
def setUpClass(cls):
# delete any existing webhooks
frappe.db.sql("DELETE FROM tabWebhook")
# Delete existing logs if any
frappe.db.sql("DELETE FROM `tabWebhook Request Log`")
# create test webhooks
cls.create_sample_webhooks()
@ -162,3 +164,18 @@ class TestWebhook(unittest.TestCase):
data = get_webhook_data(doc=self.user, webhook=self.webhook)
self.assertEqual(data, {"name": self.user.name})
def test_webhook_req_log_creation(self):
if not frappe.db.get_value('User', 'user2@integration.webhooks.test.com'):
user = frappe.get_doc({
'doctype': 'User',
'email': 'user2@integration.webhooks.test.com',
'first_name': 'user2'
}).insert()
else:
user = frappe.get_doc('User', 'user2@integration.webhooks.test.com')
webhook = frappe.get_doc('Webhook', {'webhook_doctype': 'User'})
enqueue_webhook(user, webhook)
self.assertTrue(frappe.db.get_all('Webhook Request Log', pluck='name'))

View file

@ -18,6 +18,7 @@
"html_condition",
"sb_webhook",
"request_url",
"request_method",
"cb_webhook",
"request_structure",
"sb_security",
@ -154,10 +155,18 @@
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
},
{
"default": "POST",
"fieldname": "request_method",
"fieldtype": "Select",
"label": "Request Method",
"options": "POST\nPUT\nDELETE",
"reqd": 1
}
],
"links": [],
"modified": "2021-04-14 05:35:28.532049",
"modified": "2021-05-25 11:11:28.555291",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",

View file

@ -59,7 +59,6 @@ class Webhook(Document):
if self.request_structure == "Form URL-Encoded":
self.webhook_json = None
elif self.request_structure == "JSON":
validate_json(self.webhook_json)
validate_template(self.webhook_json)
self.webhook_data = []
@ -83,18 +82,32 @@ def enqueue_webhook(doc, webhook):
for i in range(3):
try:
r = requests.post(webhook.request_url, data=json.dumps(data, default=str), headers=headers, timeout=5)
r = requests.request(method=webhook.request_method, url=webhook.request_url,
data=json.dumps(data, default=str), headers=headers, timeout=5)
r.raise_for_status()
frappe.logger().debug({"webhook_success": r.text})
log_request(webhook.request_url, headers, data, r)
break
except Exception as e:
frappe.logger().debug({"webhook_error": e, "try": i + 1})
log_request(webhook.request_url, headers, data, r)
sleep(3 * i + 1)
if i != 2:
continue
else:
raise e
def log_request(url, headers, data, res):
request_log = frappe.get_doc({
"doctype": "Webhook Request Log",
"user": frappe.session.user if frappe.session.user else None,
"url": url,
"headers": json.dumps(headers, indent=4) if headers else None,
"data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
"response": json.dumps(res.json(), indent=4) if res else None
})
request_log.save(ignore_permissions=True)
def get_webhook_headers(doc, webhook):
headers = {}
@ -129,10 +142,3 @@ def get_webhook_data(doc, webhook):
data = json.loads(data)
return data
def validate_json(string):
try:
json.loads(string)
except (TypeError, ValueError):
frappe.throw(_("Request Body consists of an invalid JSON structure"), title=_("Invalid JSON"))

View file

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
class TestWebhookRequestLog(unittest.TestCase):
pass

View file

@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Webhook Request Log', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,81 @@
{
"actions": [],
"autoname": "WEBHOOK-REQ-.#####",
"creation": "2021-05-24 21:35:59.104776",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user",
"headers",
"data",
"column_break_4",
"url",
"response"
],
"fields": [
{
"fieldname": "url",
"fieldtype": "Data",
"label": "URL",
"read_only": 1
},
{
"fieldname": "headers",
"fieldtype": "Code",
"label": "Headers",
"options": "JSON",
"read_only": 1
},
{
"fieldname": "response",
"fieldtype": "Code",
"label": "Response",
"options": "JSON",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "data",
"fieldtype": "Code",
"label": "Data",
"options": "JSON",
"read_only": 1
},
{
"fieldname": "user",
"fieldtype": "Link",
"label": "User",
"options": "User",
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-26 23:57:58.495261",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook Request Log",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class WebhookRequestLog(Document):
pass

View file

@ -8,35 +8,14 @@ from urllib.parse import parse_qs
from frappe.utils import get_request_session
from frappe import _
def make_get_request(url, auth=None, headers=None, data=None):
if not auth:
auth = ''
if not data:
data = {}
if not headers:
headers = {}
def make_request(method, url, auth=None, headers=None, data=None):
auth = auth or ''
data = data or {}
headers = headers or {}
try:
s = get_request_session()
frappe.flags.integration_request = s.get(url, data={}, auth=auth, headers=headers)
frappe.flags.integration_request.raise_for_status()
return frappe.flags.integration_request.json()
except Exception as exc:
frappe.log_error(frappe.get_traceback())
raise exc
def make_post_request(url, auth=None, headers=None, data=None):
if not auth:
auth = ''
if not data:
data = {}
if not headers:
headers = {}
try:
s = get_request_session()
frappe.flags.integration_request = s.post(url, data=data, auth=auth, headers=headers)
frappe.flags.integration_request = s.request(method, url, data=data, auth=auth, headers=headers)
frappe.flags.integration_request.raise_for_status()
if frappe.flags.integration_request.headers.get("content-type") == "text/plain; charset=utf-8":
@ -47,6 +26,15 @@ def make_post_request(url, auth=None, headers=None, data=None):
frappe.log_error()
raise exc
def make_get_request(url, **kwargs):
return make_request('GET', url, **kwargs)
def make_post_request(url, **kwargs):
return make_request('POST', url, **kwargs)
def make_put_request(url, **kwargs):
return make_request('PUT', url, **kwargs)
def create_request_log(data, integration_type, service_name, name=None, error=None):
if isinstance(data, str):
data = json.loads(data)

View file

@ -34,7 +34,8 @@ data_fieldtypes = (
'Color',
'Barcode',
'Geolocation',
'Duration'
'Duration',
'Icon'
)
no_value_fields = (

View file

@ -1,4 +1,4 @@
<svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" class="d-block" xmlns="http://www.w3.org/2000/svg">
<svg id="frappe-symbols" aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" class="d-block" xmlns="http://www.w3.org/2000/svg">
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-resting">
<path d="M7.606 3.799L8 4.302l.394-.503.106-.14c.048-.065.08-.108.129-.159a3.284 3.284 0 0 1 4.72 0c.424.434.655 1.245.65 2.278-.006 1.578-.685 2.931-1.728 4.159-1.05 1.234-2.439 2.308-3.814 3.328a.763.763 0 0 1-.914 0c-1.375-1.02-2.764-2.094-3.814-3.328C2.686 8.709 2.007 7.357 2 5.778c-.004-1.033.227-1.844.651-2.278a3.284 3.284 0 0 1 4.72 0c.05.05.081.094.129.158.028.038.061.083.106.14z"
stroke="var(--icon-stroke)"></path>

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View file

@ -39,6 +39,7 @@ import './multiselect_pills';
import './multiselect_list';
import './rating';
import './duration';
import './icon';
frappe.ui.form.make_control = function (opts) {
var control_class_name = "Control" + opts.df.fieldtype.replace(/ /g, "");

View file

@ -0,0 +1,93 @@
import Picker from '../../icon_picker/icon_picker';
frappe.ui.form.ControlIcon = class ControlIcon extends frappe.ui.form.ControlData {
make_input() {
this.df.placeholder = this.df.placeholder || __('Choose an icon');
super.make_input();
this.get_all_icons();
this.make_icon_input();
}
get_all_icons() {
frappe.symbols = [];
$("#frappe-symbols > symbol[id]").each(function() {
frappe.symbols.push(this.id.replace('icon-', ''));
});
}
make_icon_input() {
let picker_wrapper = $('<div>');
this.picker = new Picker({
parent: picker_wrapper,
icon: this.get_icon(),
icons: frappe.symbols
});
this.$wrapper.popover({
trigger: 'manual',
offset: `${-this.$wrapper.width() / 4.5}, 5`,
boundary: 'viewport',
placement: 'bottom',
template: `
<div class="popover icon-picker-popover">
<div class="picker-arrow arrow"></div>
<div class="popover-body popover-content"></div>
</div>
`,
content: () => picker_wrapper,
html: true
}).on('show.bs.popover', () => {
setTimeout(() => {
this.picker.refresh();
}, 10);
}).on('hidden.bs.popover', () => {
$('body').off('click.icon-popover');
$(window).off('hashchange.icon-popover');
});
this.picker.on_change = (icon) => {
this.set_value(icon);
};
if (!this.selected_icon) {
this.selected_icon = $(`<div class="selected-icon">${frappe.utils.icon("folder-normal", "md")}</div>`);
this.selected_icon.insertAfter(this.$input);
}
this.$wrapper.find('.selected-icon').parent().on('click', (e) => {
this.$wrapper.popover('toggle');
if (!this.get_icon()) {
this.$input.val('');
}
e.stopPropagation();
$('body').on('click.icon-popover', (ev) => {
if (!$(ev.target).parents().is('.popover')) {
this.$wrapper.popover('hide');
}
});
$(window).on('hashchange.icon-popover', () => {
this.$wrapper.popover('hide');
});
});
}
refresh() {
super.refresh();
let icon = this.get_icon();
if (this.picker && this.picker.icon !== icon) {
this.picker.icon = icon;
this.picker.refresh();
}
}
set_formatted_input(value) {
super.set_formatted_input(value);
this.$input.val(value);
this.selected_icon.find("use").attr("href", "#icon-"+(value || "folder-normal"));
this.selected_icon.toggleClass('no-value', !value);
}
get_icon() {
return this.get_value() || 'folder-normal';
}
};

View file

@ -113,6 +113,7 @@ frappe.ui.form.ControlSelect = class ControlSelect extends frappe.ui.form.Contro
var is_value_null = is_null(v.value);
var is_label_null = is_null(v.label);
var is_disabled = Boolean(v.disabled);
var is_selected = Boolean(v.selected);
if (is_value_null && is_label_null) {
value = v;
@ -126,6 +127,7 @@ frappe.ui.form.ControlSelect = class ControlSelect extends frappe.ui.form.Contro
$('<option>').html(cstr(label))
.attr('value', value)
.prop('disabled', is_disabled)
.prop('selected', is_selected)
.appendTo(this);
}
// select the first option

View file

@ -92,7 +92,7 @@ frappe.ui.form.FormTour = class FormTour {
return {
element,
name,
popover: { title, description, position: frappe.router.slug(position) },
popover: { title, description, position: frappe.router.slug(position || 'Bottom') },
onNext: on_next
};
}

View file

@ -303,6 +303,12 @@ frappe.form.formatters = {
<div class="selected-color" style="background-color: ${value}"></div>
<span class="color-value">${value}</span>
</div>` : '';
},
Icon: (value) => {
return value ? `<div>
<div class="selected-icon">${frappe.utils.icon(value, "md")}</div>
<span class="icon-value">${value}</span>
</div>` : '';
}
};

View file

@ -250,6 +250,14 @@ frappe.ui.form.Layout = class Layout {
// collapse sections
this.refresh_section_collapse();
}
if (document.activeElement) {
document.activeElement.focus();
if (document.activeElement.tagName == 'INPUT') {
document.activeElement.select();
}
}
}
refresh_sections() {

View file

@ -0,0 +1,86 @@
class Picker {
constructor(opts) {
this.parent = opts.parent;
this.width = opts.width;
this.height = opts.height;
this.set_icon(opts.icon);
this.icons = opts.icons;
this.setup_picker();
}
refresh() {
this.update_icon_selected(true);
}
setup_picker() {
this.icon_picker_wrapper = $(`
<div class="icon-picker">
<div class="search-icons">
<input type="search" placeholder="Search for icons.." class="form-control">
<span class="search-icon">${frappe.utils.icon('search', "sm")}</span>
</div>
<div class="icon-section">
<div class="icons"></div>
</div>
</div>
`);
this.parent.append(this.icon_picker_wrapper);
this.icon_wrapper = this.icon_picker_wrapper.find('.icons');
this.search_input = this.icon_picker_wrapper.find('.search-icons > input');
this.refresh();
this.setup_icons();
}
setup_icons() {
this.icons.forEach(icon => {
let $icon = $(`<div id="${icon}" class="icon-wrapper">${frappe.utils.icon(icon, "md")}</div>`);
this.icon_wrapper.append($icon);
const set_values = () => {
this.set_icon(icon);
this.update_icon_selected();
};
$icon.on('click', () => {
set_values();
});
$icon.keydown((e) => {
const key_code = e.keyCode;
if ([13, 32].includes(key_code)) {
e.preventDefault();
set_values();
}
});
this.search_input.keyup((e) => {
e.preventDefault();
this.filter_icons();
});
this.search_input.on('search', () => {
this.filter_icons();
});
});
}
filter_icons() {
let value = this.search_input.val();
if (!value) {
this.icon_wrapper.find(".icon-wrapper").removeClass('hidden');
} else {
this.icon_wrapper.find(".icon-wrapper").addClass('hidden');
this.icon_wrapper.find(`.icon-wrapper[id*='${value}']`).removeClass('hidden');
}
}
update_icon_selected(silent) {
!silent && this.on_change && this.on_change(this.get_icon());
}
set_icon(icon) {
this.icon = icon || '';
}
get_icon() {
return this.icon || '';
}
}
export default Picker;

View file

@ -98,6 +98,7 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
me.focus_on_first_input();
me.on_page_show && me.on_page_show();
$(document).trigger('frappe.ui.Dialog:shown');
$(document).off('focusin.modal');
})
.on('scroll', function() {
var $input = $('input:focus');

View file

@ -518,6 +518,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
} else {
this.page.show_form();
}
this.page.body[0].style.setProperty('--report-filter-height', this.page.page_form.css('height'));
this.page.body.parent().css('margin-bottom', 'unset');
}
set_filters(filters) {

View file

@ -49,6 +49,8 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
this.setup_columns();
super.setup_new_doc_event();
this.page.main.addClass('report-view');
this.page.body[0].style.setProperty('--report-filter-height', this.page.page_form.css('height'));
this.page.body.parent().css('margin-bottom', 'unset');
}
toggle_side_bar() {

View file

@ -1,5 +1,6 @@
@import "grid";
@import "color_picker";
@import "icon_picker";
@import "datepicker";
// password

View file

@ -0,0 +1,95 @@
.icon-picker {
font-size: var(--text-xs);
color: var(--text-muted);
--icon-picker-width: 240px;
width: var(--icon-picker-width);
.icons {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
overflow-y: scroll;
max-height: 210px;
cursor: pointer;
/* Hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
/* Hide scrollbar for Chrome, Safari and Opera */
&::-webkit-scrollbar {
display: none;
}
.icon-wrapper {
display: flex;
width: 30px;
height: 30px;
text-align: center;
align-items: center;
}
}
.search-icons {
position: relative;
input[type='search'] {
height: inherit;
padding-left: 30px;
}
.search-icon {
position: absolute;
top: 7px;
left: 7px;
}
}
}
.icon-picker-popover {
.picker-arrow {
left: 15px !important;
}
}
.frappe-control[data-fieldtype='Icon'] {
input {
padding-left: 40px;
}
.selected-icon {
cursor: pointer;
width: 22px;
height: 22px;
border-radius: 5px;
position: absolute;
top: calc(50% + 1px);
left: 8px;
content: ' ';
}
.like-disabled-input {
.icon-value {
padding-left: 25px;
}
.selected-icon {
top: 20%;
cursor: default;
}
}
}
.data-row.row {
.selected-icon {
top: calc(50% - 11px);
z-index: 2;
}
}
.dt-cell__content {
.selected-icon {
display: contents;
}
}
.dt-cell__edit, .filter-field {
.selected-icon {
top: 5px !important;
}
}

View file

@ -84,14 +84,37 @@
margin-bottom: 10px;
}
.layout-main-section .frappe-card {
--report-filter-height: 0px;
}
.report-wrapper {
overflow: auto;
.datatable {
height: calc(100vh - var(--report-filter-height) - 205px);
.dt-scrollable {
height: calc(100vh - var(--report-filter-height) - 275px);
}
}
}
.report-view {
.dt-row:last-child:not(.dt-row-filter) {
.dt-cell {
border-bottom: 1px solid var(--border-color);
.result {
min-height: 50vh !important;
.dt-row:last-child:not(.dt-row-filter) {
.dt-cell {
border-bottom: 1px solid var(--border-color);
}
}
.datatable {
height: calc(100vh - var(--report-filter-height) - 225px);
.dt-scrollable {
height: calc(100vh - var(--report-filter-height) - 295px);
}
}
}
}

View file

@ -90,19 +90,22 @@ class WebsiteSearch(FullTextSearch):
def slugs_with_web_view(_items_to_index):
all_routes = []
filters = { "has_web_view": 1, "allow_guest_to_view": 1, "index_web_pages_for_search": 1}
fields = ["name", "is_published_field", 'website_search_field']
fields = ["name", "is_published_field", "website_search_field"]
doctype_with_web_views = frappe.get_all("DocType", filters=filters, fields=fields)
for doctype in doctype_with_web_views:
if doctype.is_published_field:
docs = frappe.get_all(doctype.name, filters={doctype.is_published_field: 1}, fields=["route", doctype.website_search_field, 'title'])
fields=["route", doctype.website_search_field]
filters={doctype.is_published_field: 1},
if doctype.website_search_field:
docs = frappe.get_all(doctype.name, filters=filters, fields=fields.append("title"))
for doc in docs:
content = frappe.utils.md_to_html(getattr(doc, doctype.website_search_field))
soup = BeautifulSoup(content, "html.parser")
text_content = soup.text if soup else ""
_items_to_index += [frappe._dict(title=doc.title, content=text_content, path=doc.route)]
else:
docs = frappe.get_all(doctype.name, filters=filters, fields=fields)
all_routes += [route.route for route in docs]
return all_routes

View file

@ -436,3 +436,16 @@ class TestCommands(BaseTestCommands):
self.execute("bench version -f invalid")
self.assertEqual(self.returncode, 2)
def test_set_password(self):
from frappe.utils.password import check_password
self.execute("bench --site {site} set-password Administrator test1")
self.assertEqual(self.returncode, 0)
self.assertEqual(check_password('Administrator', 'test1'), 'Administrator')
# to release the lock taken by check_password
frappe.db.commit()
self.execute("bench --site {site} set-admin-password test2")
self.assertEqual(self.returncode, 0)
self.assertEqual(check_password('Administrator', 'test2'), 'Administrator')

View file

@ -18,8 +18,19 @@ first_lang, second_lang, third_lang, fourth_lang, fifth_lang = choices(
)
class TestTranslate(unittest.TestCase):
guest_sessions_required = [
"test_guest_request_language_resolution_with_cookie",
"test_guest_request_language_resolution_with_request_header"
]
def setUp(self):
if self._testMethodName in self.guest_sessions_required:
frappe.set_user("Guest")
def tearDown(self):
frappe.form_dict.pop("_lang", None)
if self._testMethodName in self.guest_sessions_required:
frappe.set_user("Administrator")
def test_extract_message_from_file(self):
data = frappe.translate.get_messages_from_file(translation_string_file)
@ -52,20 +63,44 @@ 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)])
return_val = get_language()
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
Case 3: frappe.form_dict._lang is not set, but preferred_language cookie is [Guest User]
"""
with patch.object(frappe.translate, "get_preferred_language_cookie", return_value=second_lang):
set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)])
return_val = get_language()
self.assertIn(return_val, [second_lang, get_parent_language(second_lang)])
def test_guest_request_language_resolution_with_request_header(self):
"""Test for frappe.translate.get_language
Case 4: frappe.form_dict._lang & preferred_language cookie is not set, but Accept-Language header is [Guest User]
"""
set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)])
return_val = get_language()
self.assertIn(return_val, [third_lang, get_parent_language(third_lang)])
def test_request_language_resolution_with_request_header(self):
"""Test for frappe.translate.get_language
Case 3: frappe.form_dict._lang & preferred_language cookie is not set, but Accept-Language header is
Case 5: frappe.form_dict._lang & preferred_language cookie is not set, but Accept-Language header is
"""
set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)])
return_val = get_language()
self.assertIn(return_val, [third_lang, get_parent_language(third_lang)])
self.assertNotIn(return_val, [third_lang, get_parent_language(third_lang)])
expected_output = [

View file

@ -27,11 +27,12 @@ def get_language(lang_list: List = None) -> str:
Order of priority for setting language:
1. Form Dict => _lang
2. Cookie => preferred_language
3. Request Header => Accept-Language
2. Cookie => preferred_language (Non authorized user)
3. Request Header => Accept-Language (Non authorized user)
4. User document => language
5. System Settings => language
"""
is_logged_in = frappe.session.user != "Guest"
# fetch language from form_dict
if frappe.form_dict._lang:
@ -41,6 +42,10 @@ def get_language(lang_list: List = None) -> str:
if language:
return language
# use language set in User or System Settings if user is logged in
if is_logged_in:
return frappe.local.lang
lang_set = set(lang_list or get_all_languages() or [])
# fetch language from cookie

View file

@ -20,7 +20,6 @@ from frappe.utils.commands import log
default_timeout = 300
queue_timeout = {
'background': 2500,
'long': 1500,
'default': 300,
'short': 300

View file

@ -54,7 +54,7 @@
"qz-tray": "^2.0.8",
"redis": "^3.1.1",
"showdown": "^1.9.1",
"snyk": "^1.518.0",
"snyk": "^1.667.0",
"socket.io": "^2.4.0",
"superagent": "^3.8.2",
"touch": "^3.1.0",

1037
yarn.lock

File diff suppressed because it is too large Load diff