@@ -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, | |||
@@ -561,30 +561,54 @@ def move(dest_dir, site): | |||
return final_new_path | |||
@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_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') | |||
@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, logout_all_sessions=False): | |||
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, | |||
@@ -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(); | |||
} | |||
} | |||
}); |
@@ -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 | |||
@@ -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) | |||
@@ -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 | |||
@@ -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 | |||
}; | |||
} | |||
@@ -264,15 +264,16 @@ export default class Grid { | |||
make_head() { | |||
// labels | |||
if (!this.header_row) { | |||
this.header_row = new GridRow({ | |||
parent: $(this.parent).find(".grid-heading-row"), | |||
parent_df: this.df, | |||
docfields: this.docfields, | |||
frm: this.frm, | |||
grid: this | |||
}); | |||
if (this.header_row) { | |||
$(this.parent).find(".grid-heading-row .grid-row").remove(); | |||
} | |||
this.header_row = new GridRow({ | |||
parent: $(this.parent).find(".grid-heading-row"), | |||
parent_df: this.df, | |||
docfields: this.docfields, | |||
frm: this.frm, | |||
grid: this | |||
}); | |||
} | |||
refresh(force) { | |||
@@ -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() { | |||
@@ -514,7 +514,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
render_skeleton() { | |||
const $row = this.get_list_row_html_skeleton( | |||
'<div><input type="checkbox" /></div>' | |||
'<div><input type="checkbox" class="render-list-checkbox"/></div>' | |||
); | |||
this.$result.append($row); | |||
} | |||
@@ -927,10 +927,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const seen = this.get_seen_class(doc); | |||
let subject_html = ` | |||
<input class="level-item list-row-checkbox hidden-xs" type="checkbox" | |||
data-name="${escape(doc.name)}"> | |||
<span class="level-item" style="margin-bottom: 1px;"> | |||
${this.get_like_html(doc)} | |||
<span class="level-item select-like"> | |||
<input class="list-row-checkbox hidden-xs" type="checkbox" | |||
data-name="${escape(doc.name)}"> | |||
<span class="list-row-like style="margin-bottom: 1px;"> | |||
${this.get_like_html(doc)} | |||
</span> | |||
</span> | |||
<span class="level-item ${seen} ellipsis" title="${escaped_subject}"> | |||
<a class="ellipsis" | |||
@@ -1127,7 +1129,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
// don't open form when checkbox, like, filterable are clicked | |||
if ( | |||
$target.hasClass("filterable") || | |||
$target.hasClass("icon-heart") || | |||
$target.hasClass("select-like") || | |||
$target.hasClass("list-row-like") || | |||
$target.is(":checkbox") | |||
) { | |||
e.stopPropagation(); | |||
@@ -58,7 +58,7 @@ | |||
} | |||
.list-row { | |||
padding: 15px; | |||
padding: 15px 15px 15px 0px; | |||
height: 45px; | |||
cursor: pointer; | |||
transition: color 0.2s; | |||
@@ -130,10 +130,15 @@ | |||
margin-left: 5px; | |||
} | |||
} | |||
.select-like { | |||
padding: 15px 0px 15px 15px; | |||
} | |||
} | |||
.list-row-head { | |||
@extend .list-row; | |||
padding: 15px; | |||
cursor: default; | |||
.list-subject { | |||
@@ -200,6 +205,10 @@ input.list-check-all, input.list-row-checkbox { | |||
--checkbox-right-margin: calc(var(--checkbox-size) / 2 + #{$level-margin-right}); | |||
} | |||
.render-list-checkbox { | |||
margin-left: 15px; | |||
} | |||
.filterable { | |||
cursor: pointer; | |||
} | |||
@@ -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 | |||
@@ -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') |
@@ -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,21 +63,45 @@ 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_request_language_resolution_with_request_header(self): | |||
def test_guest_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 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 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.assertNotIn(return_val, [third_lang, get_parent_language(third_lang)]) | |||
expected_output = [ | |||
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', 'This is some context', 2), | |||
@@ -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 | |||
@@ -20,7 +20,6 @@ from frappe.utils.commands import log | |||
default_timeout = 300 | |||
queue_timeout = { | |||
'background': 2500, | |||
'long': 1500, | |||
'default': 300, | |||
'short': 300 | |||
@@ -52,7 +52,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", | |||