Merge branch 'develop' into fix-backup

This commit is contained in:
Suraj Shetty 2021-04-16 09:08:27 +05:30 committed by GitHub
commit d8ec0cd4d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
90 changed files with 972 additions and 214 deletions

View file

@ -34,6 +34,7 @@ if PY2:
sys.setdefaultencoding("utf-8")
__version__ = '13.0.0-dev'
__title__ = "Frappe Framework"
local = Local()

View file

@ -42,6 +42,8 @@ def get_bootinfo():
bootinfo.user_info = get_user_info()
bootinfo.sid = frappe.session['sid']
bootinfo.user_groups = frappe.get_all('User Group', pluck="name")
bootinfo.modules = {}
bootinfo.module_list = []
load_desktop_data(bootinfo)

View file

@ -104,7 +104,7 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
if frappe.get_meta(doctype).issingle:
value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug)
else:
value = get_list(doctype, filters=filters, fields=fields, debug=debug, limit_page_length=1, as_dict=as_dict)
value = get_list(doctype, filters=filters, fields=fields, debug=debug, limit_page_length=1, parent=parent, as_dict=as_dict)
if as_dict:
return value[0] if value else {}

View file

@ -152,7 +152,7 @@
"fieldname": "communication_type",
"fieldtype": "Select",
"label": "Communication Type",
"options": "Communication\nComment\nChat\nBot\nNotification\nFeedback",
"options": "Communication\nComment\nChat\nBot\nNotification\nFeedback\nAutomated Message",
"read_only": 1,
"reqd": 1
},
@ -387,7 +387,7 @@
"icon": "fa fa-comment",
"idx": 1,
"links": [],
"modified": "2019-12-27 14:44:04.880373",
"modified": "2021-03-25 09:44:28.963538",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",
@ -426,13 +426,13 @@
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export":1,
"print":1,
"read": 1,
"role": "Inbox User"
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"role": "Inbox User"
},
{
"delete": 1,
@ -450,4 +450,4 @@
"title_field": "subject",
"track_changes": 1,
"track_seen": 1
}
}

View file

@ -8,8 +8,8 @@ import frappe
import json
from email.utils import formataddr
from frappe.core.utils import get_parent_doc
from frappe.utils import (get_url, get_formatted_email, cint,
validate_email_address, split_emails, parse_addr, get_datetime)
from frappe.utils import (get_url, get_formatted_email, cint, list_to_str,
validate_email_address, split_emails, parse_addr, get_datetime)
from frappe.email.email_body import get_message_id
import frappe.email.smtp
import time
@ -20,7 +20,8 @@ from frappe.utils.background_jobs import enqueue
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False,
print_html=None, print_format=None, attachments='[]', send_me_a_copy=False, cc=None, bcc=None,
flags=None, read_receipt=None, print_letterhead=True, email_template=None):
flags=None, read_receipt=None, print_letterhead=True, email_template=None, communication_type=None,
ignore_permissions=False):
"""Make a new communication.
:param doctype: Reference DocType.
@ -42,15 +43,17 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
is_error_report = (doctype=="User" and name==frappe.session.user and subject=="Error Report")
send_me_a_copy = cint(send_me_a_copy)
if doctype and name and not is_error_report and not frappe.has_permission(doctype, "email", name) and not (flags or {}).get('ignore_doctype_permissions'):
raise frappe.PermissionError("You are not allowed to send emails related to: {doctype} {name}".format(
doctype=doctype, name=name))
if not ignore_permissions:
if doctype and name and not is_error_report and not frappe.has_permission(doctype, "email", name) and not (flags or {}).get('ignore_doctype_permissions'):
raise frappe.PermissionError("You are not allowed to send emails related to: {doctype} {name}".format(
doctype=doctype, name=name))
if not sender:
sender = get_formatted_email(frappe.session.user)
if isinstance(recipients, list):
recipients = ', '.join(recipients)
recipients = list_to_str(recipients) if isinstance(recipients, list) else recipients
cc = list_to_str(cc) if isinstance(cc, list) else cc
bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc
comm = frappe.get_doc({
"doctype":"Communication",
@ -68,7 +71,8 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
"email_template": email_template,
"message_id":get_message_id().strip(" <>"),
"read_receipt":read_receipt,
"has_attachment": 1 if attachments else 0
"has_attachment": 1 if attachments else 0,
"communication_type": communication_type
}).insert(ignore_permissions=True)
comm.save(ignore_permissions=True)

View file

@ -53,7 +53,8 @@
"fieldname": "import_file",
"fieldtype": "Attach",
"in_list_view": 1,
"label": "Import File"
"label": "Import File",
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
},
{
"fieldname": "import_preview",
@ -156,10 +157,11 @@
"description": "Must be a publicly accessible Google Sheets URL",
"fieldname": "google_sheets_url",
"fieldtype": "Data",
"label": "Import from Google Sheets"
"label": "Import from Google Sheets",
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
},
{
"depends_on": "eval:doc.google_sheets_url && !doc.__unsaved",
"depends_on": "eval:doc.google_sheets_url && !doc.__unsaved && ['Success', 'Partial Success'].includes(doc.status)",
"fieldname": "refresh_google_sheet",
"fieldtype": "Button",
"label": "Refresh Google Sheet"
@ -167,7 +169,7 @@
],
"hide_toolbar": 1,
"links": [],
"modified": "2020-06-24 14:33:03.173876",
"modified": "2021-04-11 01:50:42.074623",
"modified_by": "Administrator",
"module": "Core",
"name": "Data Import",

View file

@ -15,8 +15,9 @@ frappe.ui.form.on('Document Naming Rule', {
}).map((d) => {
return {label: `${d.label} (${d.fieldname})`, value: d.fieldname};
});
frappe.meta.get_docfield('Document Naming Rule Condition', 'field', frm.doc.name).options = fieldnames;
frm.refresh_field('conditions');
frm.fields_dict.conditions.grid.update_docfield_property(
'field', 'options', fieldnames
);
});
}
}

View file

@ -970,12 +970,22 @@ def get_files_in_folder(folder, start=0, page_length=20):
start = cint(start)
page_length = cint(page_length)
files = frappe.db.get_all('File',
attachment_folder = frappe.db.get_value('File',
'Home/Attachments',
['name', 'file_name', 'file_url', 'is_folder', 'modified'],
as_dict=1
)
files = frappe.db.get_list('File',
{ 'folder': folder },
['name', 'file_name', 'file_url', 'is_folder', 'modified'],
start=start,
page_length=page_length + 1
)
if folder == 'Home' and attachment_folder not in files:
files.insert(0, attachment_folder)
return {
'files': files[:page_length],
'has_more': len(files) > page_length

View file

@ -8,7 +8,7 @@ import frappe
import os
import unittest
from frappe import _
from frappe.core.doctype.file.file import move_file
from frappe.core.doctype.file.file import move_file, get_files_in_folder
from frappe.utils import get_files_path
# test_records = frappe.get_test_records('File')
@ -412,3 +412,61 @@ class TestAttachment(unittest.TestCase):
})
self.assertTrue(exists)
class TestAttachmentsAccess(unittest.TestCase):
def test_attachments_access(self):
frappe.set_user('test4@example.com')
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
frappe.get_doc({
"doctype": "File",
"file_name": 'test_user.txt',
"attached_to_doctype": self.attached_to_doctype,
"attached_to_name": self.attached_to_docname,
"content": 'Testing User'
}).insert()
frappe.get_doc({
"doctype": "File",
"file_name": "test_user_home.txt",
"content": 'User Home',
}).insert()
frappe.set_user('test@example.com')
frappe.get_doc({
"doctype": "File",
"file_name": 'test_system_manager.txt',
"attached_to_doctype": self.attached_to_doctype,
"attached_to_name": self.attached_to_docname,
"content": 'Testing System Manager'
}).insert()
frappe.get_doc({
"doctype": "File",
"file_name": "test_sm_home.txt",
"content": 'System Manager Home',
}).insert()
system_manager_files = [file.file_name for file in get_files_in_folder('Home')['files']]
system_manager_attachments_files = [file.file_name for file in get_files_in_folder('Home/Attachments')['files']]
frappe.set_user('test4@example.com')
user_files = [file.file_name for file in get_files_in_folder('Home')['files']]
user_attachments_files = [file.file_name for file in get_files_in_folder('Home/Attachments')['files']]
self.assertIn('test_sm_home.txt', system_manager_files)
self.assertNotIn('test_sm_home.txt', user_files)
self.assertIn('test_user_home.txt', system_manager_files)
self.assertIn('test_user_home.txt', user_files)
self.assertIn('test_system_manager.txt', system_manager_attachments_files)
self.assertNotIn('test_system_manager.txt', user_attachments_files)
self.assertIn('test_user.txt', system_manager_attachments_files)
self.assertIn('test_user.txt', user_attachments_files)
frappe.set_user('Administrator')
frappe.db.rollback()

View file

@ -37,7 +37,10 @@ def run_background(prepared_report):
custom_report_doc = report
reference_report = custom_report_doc.reference_report
report = frappe.get_doc("Report", reference_report)
report.custom_columns = custom_report_doc.json
if custom_report_doc.json:
data = json.loads(custom_report_doc.json)
if data:
report.custom_columns = data["columns"]
result = generate_report_result(
report=report,

View file

@ -25,7 +25,7 @@ frappe.ui.form.on('Report', {
}
}, "fa fa-table");
if (doc.is_standard === "Yes") {
if (doc.is_standard === "Yes" && frm.perm[0].write) {
frm.add_custom_button(doc.disabled ? __("Enable Report") : __("Disable Report"), function() {
frm.call('toggle_disable', {
disable: doc.disabled ? 0 : 1

View file

@ -307,6 +307,9 @@ class Report(Document):
@frappe.whitelist()
def toggle_disable(self, disable):
if not self.has_permission('write'):
frappe.throw(_("You are not allowed to edit the report."))
self.db_set("disabled", cint(disable))
@frappe.whitelist()

View file

@ -201,3 +201,27 @@ result = [
# check values
self.assertTrue('System User' in [d.get('type') for d in data[1]])
def test_toggle_disabled(self):
"""Make sure that authorization is respected.
"""
# Assuming that there will be reports in the system.
reports = frappe.get_all(doctype='Report', limit=1)
report_name = reports[0]['name']
doc = frappe.get_doc('Report', report_name)
status = doc.disabled
# User has write permission on reports and should pass through
frappe.set_user('test@example.com')
doc.toggle_disable(not status)
doc.reload()
self.assertNotEqual(status, doc.disabled)
# User has no write permission on reports, permission error is expected.
frappe.set_user('test1@example.com')
doc = frappe.get_doc('Report', report_name)
with self.assertRaises(frappe.exceptions.ValidationError):
doc.toggle_disable(1)
# Set user back to administrator
frappe.set_user('Administrator')

View file

@ -229,6 +229,28 @@ class TestUser(unittest.TestCase):
self.assertEqual(extract_mentions(comment)[0], "test_user@example.com")
self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com")
doc = frappe.get_doc({
'doctype': 'User Group',
'name': 'Team',
'user_group_members': [{
'user': 'test@example.com'
}, {
'user': 'test1@example.com'
}]
})
doc.insert(ignore_if_duplicate=True)
comment = '''
<div>
Testing comment for
<span class="mention" data-id="Team" data-value="Team" data-is-group="true" data-denotation-char="@">
<span><span class="ql-mention-denotation-char">@</span>Team</span>
</span>
please check
</div>
'''
self.assertListEqual(extract_mentions(comment), ['test@example.com', 'test1@example.com'])
def test_rate_limiting_for_reset_password(self):
# Allow only one reset request for a day
frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 1)
@ -247,29 +269,31 @@ class TestUser(unittest.TestCase):
self.assertEqual(res1.status_code, 200)
self.assertEqual(res2.status_code, 417)
def test_user_rollback(self):
""" """
frappe.db.commit()
frappe.db.begin()
user_id = str(uuid.uuid4())
email = f'{user_id}@example.com'
try:
frappe.flags.in_import = True # disable throttling
frappe.get_doc(dict(
doctype='User',
email=email,
first_name=user_id,
)).insert()
finally:
frappe.flags.in_import = False
# def test_user_rollback(self):
# """
# FIXME: This is failing with PR #12693 as Rollback can't happen if notifications sent on user creation.
# Make sure that notifications disabled.
# """
# frappe.db.commit()
# frappe.db.begin()
# user_id = str(uuid.uuid4())
# email = f'{user_id}@example.com'
# try:
# frappe.flags.in_import = True # disable throttling
# frappe.get_doc(dict(
# doctype='User',
# email=email,
# first_name=user_id,
# )).insert()
# finally:
# frappe.flags.in_import = False
# Check user has been added
self.assertIsNotNone(frappe.db.get("User", {"email": email}))
# Check that rollback works
frappe.db.rollback()
self.assertIsNone(frappe.db.get("User", {"email": email}))
# # Check user has been added
# self.assertIsNotNone(frappe.db.get("User", {"email": email}))
# # Check that rollback works
# frappe.db.rollback()
# self.assertIsNone(frappe.db.get("User", {"email": email}))
def delete_contact(user):
frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user)

View file

@ -1018,8 +1018,16 @@ def extract_mentions(txt):
soup = BeautifulSoup(txt, 'html.parser')
emails = []
for mention in soup.find_all(class_='mention'):
if mention.get('data-is-group') == 'true':
try:
user_group = frappe.get_cached_doc('User Group', mention['data-id'])
emails += [d.user for d in user_group.user_group_members]
except frappe.DoesNotExistError:
pass
continue
email = mention['data-id']
emails.append(email)
return emails
def handle_password_test_fail(result):

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestUserGroup(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('User Group', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,48 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2021-04-12 15:17:24.751710",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user_group_members"
],
"fields": [
{
"fieldname": "user_group_members",
"fieldtype": "Table MultiSelect",
"label": "User Group Members",
"options": "User Group Member",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-04-15 16:12:31.455401",
"modified_by": "Administrator",
"module": "Core",
"name": "User Group",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "All"
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
import frappe
class UserGroup(Document):
def after_insert(self):
frappe.publish_realtime('user_group_added', self.name)
def on_trash(self):
frappe.publish_realtime('user_group_deleted', self.name)

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestUserGroupMember(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('User Group Member', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,32 @@
{
"actions": [],
"creation": "2021-04-12 15:16:29.279107",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user"
],
"fields": [
{
"fieldname": "user",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-12 15:17:18.773046",
"modified_by": "Administrator",
"module": "Core",
"name": "User Group Member",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

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

View file

@ -108,7 +108,7 @@ class UserType(Document):
frappe.db.set_value('Custom DocPerm', docperm, values)
def add_select_perm_doctypes(self):
if not frappe.flags.in_patch and not frappe.conf.developer_mode:
if frappe.flags.ignore_select_perm:
return
self.select_doctypes = []
@ -122,7 +122,8 @@ class UserType(Document):
for child_table in doc.get_table_fields():
child_doc = frappe.get_meta(child_table.options)
self.prepare_select_perm_doctypes(child_doc, user_doctypes, select_doctypes)
if not child_doc.istable:
self.prepare_select_perm_doctypes(child_doc, user_doctypes, select_doctypes)
if select_doctypes:
select_doctypes = set(select_doctypes)

View file

@ -40,6 +40,8 @@ class CustomField(Document):
frappe.throw(_("A field with the name '{}' already exists in doctype {}.").format(self.fieldname, self.dt))
def validate(self):
from frappe.custom.doctype.customize_form.customize_form import CustomizeForm
meta = frappe.get_meta(self.dt, cached=False)
fieldnames = [df.fieldname for df in meta.get("fields")]
@ -49,7 +51,11 @@ class CustomField(Document):
if self.insert_after and self.insert_after in fieldnames:
self.idx = fieldnames.index(self.insert_after) + 1
self._old_fieldtype = self.db_get('fieldtype')
old_fieldtype = self.db_get('fieldtype')
is_fieldtype_changed = (not self.is_new()) and (old_fieldtype != self.fieldtype)
if is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype):
frappe.throw(_("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype))
if not self.fieldname:
frappe.throw(_("Fieldname not set for Custom Field"))

View file

@ -401,22 +401,18 @@ class CustomizeForm(Document):
return property_value
def validate_fieldtype_change(self, df, old_value, new_value):
allowed = False
self.check_length_for_fieldtypes = []
for allowed_changes in ALLOWED_FIELDTYPE_CHANGE:
if (old_value in allowed_changes and new_value in allowed_changes):
allowed = True
old_value_length = cint(frappe.db.type_map.get(old_value)[1])
new_value_length = cint(frappe.db.type_map.get(new_value)[1])
allowed = self.allow_fieldtype_change(old_value, new_value)
if allowed:
old_value_length = cint(frappe.db.type_map.get(old_value)[1])
new_value_length = cint(frappe.db.type_map.get(new_value)[1])
# Ignore fieldtype check validation if new field type has unspecified maxlength
# Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated
if new_value_length and (old_value_length > new_value_length):
self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value})
self.validate_fieldtype_length()
else:
self.flags.update_db = True
break
# Ignore fieldtype check validation if new field type has unspecified maxlength
# Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated
if new_value_length and (old_value_length > new_value_length):
self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value})
self.validate_fieldtype_length()
else:
self.flags.update_db = True
if not allowed:
frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx))
@ -458,6 +454,14 @@ class CustomizeForm(Document):
reset_customization(self.doc_type)
self.fetch_to_customize()
@classmethod
def allow_fieldtype_change(self, old_type: str, new_type: str) -> bool:
""" allow type change, if both old_type and new_type are in same field group.
field groups are defined in ALLOWED_FIELDTYPE_CHANGE variables.
"""
in_field_group = lambda group: (old_type in group) and (new_type in group)
return any(map(in_field_group, ALLOWED_FIELDTYPE_CHANGE))
def reset_customization(doctype):
setters = frappe.get_all("Property Setter", filters={
'doc_type': doctype,

View file

@ -985,7 +985,7 @@ class Database(object):
def log_touched_tables(self, query, values=None):
if values:
query = frappe.safe_decode(self._cursor.mogrify(query, values))
if query.strip().lower().split()[0] in ('insert', 'delete', 'update', 'alter'):
if query.strip().lower().split()[0] in ('insert', 'delete', 'update', 'alter', 'drop', 'rename'):
# single_word_regex is designed to match following patterns
# `tabXxx`, tabXxx and "tabXxx"

View file

@ -63,7 +63,7 @@ class Workspace:
for section in cards:
links = loads(section.get('links')) if isinstance(section.get('links'), string_types) else section.get('links')
for item in links:
if self.is_item_allowed(item.get('name'), item.get('type')):
if self.is_item_allowed(item.get('link_to'), item.get('link_type')):
return True
def _in_active_domains(item):

View file

@ -89,10 +89,16 @@ def get_docinfo(doc=None, doctype=None, name=None):
doc = frappe.get_doc(doctype, name)
if not doc.has_permission("read"):
raise frappe.PermissionError
all_communications = _get_communications(doc.doctype, doc.name)
automated_messages = filter(lambda x: x['communication_type'] == 'Automated Message', all_communications)
communications_except_auto_messages = filter(lambda x: x['communication_type'] != 'Automated Message', all_communications)
frappe.response["docinfo"] = {
"attachments": get_attachments(doc.doctype, doc.name),
"attachment_logs": get_comments(doc.doctype, doc.name, 'attachment'),
"communications": _get_communications(doc.doctype, doc.name),
"communications": communications_except_auto_messages,
"automated_messages": automated_messages,
'comments': get_comments(doc.doctype, doc.name),
'total_comments': len(json.loads(doc.get('_comments') or '[]')),
'versions': get_versions(doc),
@ -187,7 +193,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
C.sender, C.sender_full_name, C.cc, C.bcc,
C.creation AS creation, C.subject, C.delivery_status,
C._liked_by, C.reference_doctype, C.reference_name,
C.read_by_recipient, C.rating
C.read_by_recipient, C.rating, C.recipients
'''
conditions = ''
@ -206,7 +212,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
part1 = '''
SELECT {fields}
FROM `tabCommunication` as C
WHERE C.communication_type IN ('Communication', 'Feedback')
WHERE C.communication_type IN ('Communication', 'Feedback', 'Automated Message')
AND (C.reference_doctype = %(doctype)s AND C.reference_name = %(name)s)
{conditions}
'''.format(fields=fields, conditions=conditions)
@ -216,7 +222,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
SELECT {fields}
FROM `tabCommunication` as C
INNER JOIN `tabCommunication Link` ON C.name=`tabCommunication Link`.parent
WHERE C.communication_type IN ('Communication', 'Feedback')
WHERE C.communication_type IN ('Communication', 'Feedback', 'Automated Message')
AND `tabCommunication Link`.link_doctype = %(doctype)s AND `tabCommunication Link`.link_name = %(name)s
{conditions}
'''.format(fields=fields, conditions=conditions)
@ -304,4 +310,4 @@ def get_additional_timeline_content(doctype, docname):
for method in methods_for_all_doctype + methods_for_current_doctype:
contents.extend(frappe.get_attr(method)(doctype, docname) or [])
return contents
return contents

View file

@ -36,7 +36,10 @@ def get_report_doc(report_name):
reference_report = custom_report_doc.reference_report
doc = frappe.get_doc("Report", reference_report)
doc.custom_report = report_name
doc.custom_columns = custom_report_doc.json
if custom_report_doc.json:
data = json.loads(custom_report_doc.json)
if data:
doc.custom_columns = data["columns"]
doc.is_custom_report = True
if not doc.is_permitted():
@ -83,7 +86,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
if report.custom_columns:
# saved columns (with custom columns / with different column order)
columns = json.loads(report.custom_columns)
columns = report.custom_columns
# unsaved custom_columns
if custom_columns:
@ -524,9 +527,12 @@ def save_report(reference_report, report_name, columns):
"report_type": "Custom Report",
},
)
if docname:
report = frappe.get_doc("Report", docname)
report.update({"json": columns})
existing_jd = json.loads(report.json)
existing_jd["columns"] = json.loads(columns)
report.update({"json": json.dumps(existing_jd, separators=(',', ':'))})
report.save()
frappe.msgprint(_("Report updated successfully"))
@ -536,7 +542,7 @@ def save_report(reference_report, report_name, columns):
{
"doctype": "Report",
"report_name": report_name,
"json": columns,
"json": f'{{"columns":{columns}}}',
"ref_doctype": report_doc.ref_doctype,
"is_standard": "No",
"report_type": "Custom Report",

View file

@ -252,7 +252,7 @@ def make_links(columns, data):
elif col.fieldtype == "Dynamic Link":
if col.options and row.get(col.fieldname) and row.get(col.options):
row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname])
elif col.fieldtype == "Currency":
elif col.fieldtype == "Currency" and row.get(col.fieldname):
row[col.fieldname] = frappe.format_value(row[col.fieldname], col)
return columns, data

View file

@ -85,14 +85,11 @@ frappe.notification = {
}
// set email recipient options
frappe.meta.get_docfield(
'Notification Recipient',
frm.fields_dict.recipients.grid.update_docfield_property(
'receiver_by_document_field',
// set first option as blank to allow notification not to be defaulted to the owner
frm.doc.name
).options = [''].concat(["owner"]).concat(receiver_fields);
frm.fields_dict.recipients.grid.refresh();
'options',
[''].concat(["owner"]).concat(receiver_fields)
);
});
},
setup_example_message: function(frm) {

View file

@ -189,6 +189,7 @@ def get_context(context):
def send_an_email(self, doc, context):
from email.utils import formataddr
from frappe.core.doctype.communication.email import make as make_communication
subject = self.subject
if "{" in subject:
subject = frappe.render_template(self.subject, context)
@ -199,6 +200,7 @@ def get_context(context):
return
sender = None
message = frappe.render_template(self.message, context)
if self.sender and self.sender_email:
sender = formataddr((self.sender, self.sender_email))
frappe.sendmail(recipients = recipients,
@ -206,7 +208,7 @@ def get_context(context):
sender = sender,
cc = cc,
bcc = bcc,
message = frappe.render_template(self.message, context),
message = message,
reference_doctype = doc.doctype,
reference_name = doc.name,
attachments = attachments,
@ -214,6 +216,23 @@ def get_context(context):
print_letterhead = ((attachments
and attachments[0].get('print_letterhead')) or False))
# Add mail notification to communication list
# No need to add if it is already a communication.
if doc.doctype != 'Communication':
make_communication(doctype=doc.doctype,
name=doc.name,
content=message,
subject=subject,
sender=sender,
recipients=recipients,
communication_medium="Email",
send_email=False,
attachments=attachments,
cc=cc,
bcc=bcc,
communication_type='Automated Message',
ignore_permissions=True)
def send_a_slack_msg(self, doc, context):
send_slack_message(
webhook_url=self.slack_webhook_url,

View file

@ -44,6 +44,8 @@ class TestNotification(unittest.TestCase):
frappe.set_user("Administrator")
def test_new_and_save(self):
"""Check creating a new communication triggers a notification.
"""
communication = frappe.new_doc("Communication")
communication.communication_type = 'Comment'
communication.subject = "test"
@ -54,6 +56,7 @@ class TestNotification(unittest.TestCase):
"reference_name": communication.name, "status":"Not Sent"}))
frappe.db.sql("""delete from `tabEmail Queue`""")
communication.reload()
communication.content = "test 2"
communication.save()
@ -64,6 +67,8 @@ class TestNotification(unittest.TestCase):
communication.name, 'subject'), '__testing__')
def test_condition(self):
"""Check notification is triggered based on a condition.
"""
event = frappe.new_doc("Event")
event.subject = "test",
event.event_type = "Private"
@ -79,6 +84,11 @@ class TestNotification(unittest.TestCase):
self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Event",
"reference_name": event.name, "status":"Not Sent"}))
# Make sure that we track the triggered notifications in communication doctype.
self.assertTrue(frappe.db.get_value("Communication", {"reference_doctype": "Event",
"reference_name": event.name, "communication_type": 'Automated Message'}))
def test_invalid_condition(self):
frappe.set_user("Administrator")
notification = frappe.new_doc("Notification")

View file

@ -2,7 +2,9 @@
# License: The MIT License
import unittest
import frappe
from frappe.email.smtp import SMTPServer
from frappe.email.smtp import get_outgoing_email_account
class TestSMTP(unittest.TestCase):
def test_smtp_ssl_session(self):
@ -13,6 +15,57 @@ class TestSMTP(unittest.TestCase):
for port in [None, 0, 587, "587"]:
make_server(port, 0, 1)
def test_get_email_account(self):
existing_email_accounts = frappe.get_all("Email Account", fields = ["name", "enable_outgoing", "default_outgoing", "append_to"])
unset_details = {
"enable_outgoing": 0,
"default_outgoing": 0,
"append_to": None
}
for email_account in existing_email_accounts:
frappe.db.set_value('Email Account', email_account['name'], unset_details)
# remove mail_server config so that test@example.com is not created
mail_server = frappe.conf.get('mail_server')
del frappe.conf['mail_server']
frappe.local.outgoing_email_account = {}
frappe.local.outgoing_email_account = {}
# lowest preference given to email account with default incoming enabled
create_email_account(email_id="default_outgoing_enabled@gmail.com", password="***", enable_outgoing = 1, default_outgoing=1)
self.assertEqual(get_outgoing_email_account().email_id, "default_outgoing_enabled@gmail.com")
frappe.local.outgoing_email_account = {}
# highest preference given to email account with append_to matching
create_email_account(email_id="append_to@gmail.com", password="***", enable_outgoing = 1, default_outgoing=1, append_to="Blog Post")
self.assertEqual(get_outgoing_email_account(append_to="Blog Post").email_id, "append_to@gmail.com")
# add back the mail_server
frappe.conf['mail_server'] = mail_server
for email_account in existing_email_accounts:
set_details = {
"enable_outgoing": email_account['enable_outgoing'],
"default_outgoing": email_account['default_outgoing'],
"append_to": email_account['append_to']
}
frappe.db.set_value('Email Account', email_account['name'], set_details)
def create_email_account(email_id, password, enable_outgoing, default_outgoing=0, append_to=None):
email_dict = {
"email_id": email_id,
"passsword": password,
"enable_outgoing":enable_outgoing ,
"default_outgoing":default_outgoing ,
"enable_incoming": 1,
"append_to":append_to,
"is_dummy_password": 1,
"smtp_server": "localhost"
}
email_account = frappe.new_doc('Email Account')
email_account.update(email_dict)
email_account.save()
def make_server(port, ssl, tls):
server = SMTPServer(
@ -22,4 +75,4 @@ def make_server(port, ssl, tls):
use_tls = tls
)
server.sess
server.sess

View file

@ -21,7 +21,9 @@ def run_webhooks(doc, method):
if webhooks is None:
# query webhooks
webhooks_list = frappe.get_all('Webhook',
fields=["name", "`condition`", "webhook_docevent", "webhook_doctype"])
fields=["name", "`condition`", "webhook_docevent", "webhook_doctype"],
filters={"enabled": True}
)
# make webhooks map for cache
webhooks = {}

View file

@ -10,6 +10,44 @@ from frappe.integrations.doctype.webhook.webhook import get_webhook_headers, get
class TestWebhook(unittest.TestCase):
@classmethod
def setUpClass(cls):
# delete any existing webhooks
frappe.db.sql("DELETE FROM tabWebhook")
# create test webhooks
cls.create_sample_webhooks()
@classmethod
def create_sample_webhooks(cls):
samples_webhooks_data = [
{
"webhook_doctype": "User",
"webhook_docevent": "after_insert",
"request_url": "https://httpbin.org/post",
"condition": "doc.email",
"enabled": True
},
{
"webhook_doctype": "User",
"webhook_docevent": "after_insert",
"request_url": "https://httpbin.org/post",
"condition": "doc.first_name",
"enabled": False
}
]
cls.sample_webhooks = []
for wh_fields in samples_webhooks_data:
wh = frappe.new_doc("Webhook")
wh.update(wh_fields)
wh.insert()
cls.sample_webhooks.append(wh)
@classmethod
def tearDownClass(cls):
# delete any existing webhooks
frappe.db.sql("DELETE FROM tabWebhook")
def setUp(self):
# retrieve or create a User webhook for `after_insert`
webhook_fields = {
@ -30,10 +68,37 @@ class TestWebhook(unittest.TestCase):
self.user.email = frappe.mock("email")
self.user.save()
# Create another test user specific to this test
self.test_user = frappe.new_doc("User")
self.test_user.email = "user1@integration.webhooks.test.com"
self.test_user.first_name = "user1"
def tearDown(self) -> None:
self.user.delete()
self.test_user.delete()
super().tearDown()
def test_webhook_trigger_with_enabled_webhooks(self):
"""Test webhook trigger for enabled webhooks"""
frappe.cache().delete_value('webhooks')
frappe.flags.webhooks = None
# Insert the user to db
self.test_user.insert()
self.assertTrue("User" in frappe.flags.webhooks)
# only 1 hook (enabled) must be queued
self.assertEqual(
len(frappe.flags.webhooks.get("User")),
1
)
self.assertTrue(self.test_user.email in frappe.flags.webhooks_executed)
self.assertEqual(
frappe.flags.webhooks_executed.get(self.test_user.email)[0],
self.sample_webhooks[0].name
)
def test_validate_doc_events(self):
"Test creating a submit-related webhook for a non-submittable DocType"

View file

@ -25,7 +25,9 @@ frappe.webhook = {
}
}
frappe.meta.get_docfield("Webhook Data", "fieldname", frm.doc.name).options = [""].concat(fields);
frm.fields_dict.webhook_data.grid.update_docfield_property(
'fieldname', 'options', [""].concat(fields)
);
});
}
},

View file

@ -11,6 +11,7 @@
"webhook_doctype",
"cb_doc_events",
"webhook_docevent",
"enabled",
"sb_condition",
"condition",
"cb_condition",
@ -147,10 +148,16 @@
"fieldname": "webhook_secret",
"fieldtype": "Password",
"label": "Webhook Secret"
},
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
}
],
"links": [],
"modified": "2020-01-13 01:53:04.459968",
"modified": "2021-04-14 05:35:28.532049",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",

View file

@ -589,7 +589,7 @@ class DatabaseQuery(object):
else:
#if has if_owner permission skip user perm check
if role_permissions.get("if_owner", {}).get("read"):
if role_permissions.get("has_if_owner_enabled") and role_permissions.get("if_owner", {}):
self.match_conditions.append("`tab{0}`.`owner` = {1}".format(self.doctype,
frappe.db.escape(self.user, percent=False)))
# add user permission only if role has read perm

View file

@ -157,10 +157,10 @@ def update_naming_series(doc):
if doc.meta.autoname:
if doc.meta.autoname.startswith("naming_series:") \
and getattr(doc, "naming_series", None):
revert_series_if_last(doc.naming_series, doc.name)
revert_series_if_last(doc.naming_series, doc.name, doc)
elif doc.meta.autoname.split(":")[0] not in ("Prompt", "field", "hash"):
revert_series_if_last(doc.meta.autoname, doc.name)
revert_series_if_last(doc.meta.autoname, doc.name, doc)
def delete_from_table(doctype, name, ignore_doctypes, doc):
if doctype!="DocType" and doctype==name:

View file

@ -198,7 +198,7 @@ def getseries(key, digits):
return ('%0'+str(digits)+'d') % current
def revert_series_if_last(key, name):
def revert_series_if_last(key, name, doc=None):
if ".#" in key:
prefix, hashes = key.rsplit(".", 1)
if "#" not in hashes:
@ -207,7 +207,7 @@ def revert_series_if_last(key, name):
prefix = key
if '.' in prefix:
prefix = parse_naming_series(prefix.split('.'))
prefix = parse_naming_series(prefix.split('.'), doc=doc)
count = cint(name.replace(prefix, ""))
current = frappe.db.sql("SELECT `current` FROM `tabSeries` WHERE `name`=%s FOR UPDATE", (prefix,))

View file

@ -334,3 +334,4 @@ frappe.patches.v13_0.delete_package_publish_tool
frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings
frappe.patches.v13_0.remove_twilio_settings
frappe.patches.v12_0.rename_uploaded_files_with_proper_name
frappe.patches.v13_0.queryreport_columns

View file

@ -0,0 +1,22 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
import json
def execute():
"""Convert Query Report json to support other content"""
records = frappe.get_all('Report',
filters={
"json": ["!=", ""]
},
fields=["name", "json"]
)
for record in records:
jstr = record["json"]
data = json.loads(jstr)
if isinstance(data, list):
# double escape braces
jstr = f'{{"columns":{jstr}}}'
frappe.db.update('Report', record["name"], "json", jstr)

View file

@ -3,7 +3,7 @@ import frappe
def execute():
frappe.reload_doc('website', 'doctype', 'website_theme_ignore_app')
frappe.reload_doc('website', 'doctype', 'color')
frappe.reload_doctype('Website Theme')
frappe.reload_doc('website', 'doctype', 'website_theme', force=True)
for theme in frappe.get_all('Website Theme'):
doc = frappe.get_doc('Website Theme', theme.name)

View file

@ -108,11 +108,18 @@ def get_doc_permissions(doc, user=None, ptype=None):
meta = frappe.get_meta(doc.doctype)
def is_user_owner():
doc_owner = doc.get('owner') or ''
doc_owner = doc_owner.lower()
session_user = frappe.session.user.lower()
return doc_owner == session_user
if has_controller_permissions(doc, ptype, user=user) == False :
push_perm_check_log('Not allowed via controller permission check')
return {ptype: 0}
permissions = copy.deepcopy(get_role_permissions(meta, user=user))
permissions = copy.deepcopy(get_role_permissions(meta, user=user, is_owner=is_user_owner()))
if not cint(meta.is_submittable):
permissions["submit"] = 0
@ -120,13 +127,8 @@ def get_doc_permissions(doc, user=None, ptype=None):
if not cint(meta.allow_import):
permissions["import"] = 0
def is_user_owner():
doc_owner = doc.get('owner') or ''
doc_owner = doc_owner.lower()
session_user = frappe.session.user.lower()
return doc_owner == session_user
if is_user_owner():
# Override with `if_owner` perms irrespective of user
if permissions.get('has_if_owner_enabled'):
# apply owner permissions on top of existing permissions
# some access might be only for the owner
# eg. everyone might have read access but only owner can delete
@ -143,7 +145,7 @@ def get_doc_permissions(doc, user=None, ptype=None):
return permissions
def get_role_permissions(doctype_meta, user=None):
def get_role_permissions(doctype_meta, user=None, is_owner=None):
"""
Returns dict of evaluated role permissions like
{
@ -183,6 +185,8 @@ def get_role_permissions(doctype_meta, user=None):
applicable_permissions = list(filter(is_perm_applicable, getattr(doctype_meta, 'permissions', [])))
has_if_owner_enabled = any(p.get('if_owner', 0) for p in applicable_permissions)
perms['has_if_owner_enabled'] = has_if_owner_enabled
for ptype in rights:
pvalue = any(p.get(ptype, 0) for p in applicable_permissions)
# check if any perm object allows perm type
@ -191,7 +195,7 @@ def get_role_permissions(doctype_meta, user=None):
and has_if_owner_enabled
and not has_permission_without_if_owner_enabled(ptype)
and ptype != 'create'):
perms['if_owner'][ptype] = 1
perms['if_owner'][ptype] = cint(pvalue and is_owner)
# has no access if not owner
# only provide select or read access so that user is able to at-least access list
# (and the documents will be filtered based on owner sin further checks)

View file

@ -494,7 +494,7 @@
</symbol>
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-heart-active">
<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="none">
stroke="var(--icon-stroke)">
</path>
</symbol>
<symbol viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" id="icon-solid-error">

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View file

@ -113,7 +113,7 @@ frappe.Application = Class.extend({
dialog.get_close_btn().toggle(false);
});
this.setup_social_listeners();
this.setup_user_group_listeners();
// listen to build errors
this.setup_build_error_listener();
@ -592,11 +592,12 @@ frappe.Application = Class.extend({
}
},
setup_social_listeners() {
frappe.realtime.on('mention', (message) => {
if (frappe.get_route()[0] !== 'social') {
frappe.show_alert(message);
}
setup_user_group_listeners() {
frappe.realtime.on('user_group_added', (user_group) => {
frappe.boot.user_groups && frappe.boot.user_groups.push(user_group);
});
frappe.realtime.on('user_group_deleted', (user_group) => {
frappe.boot.user_groups = (frappe.boot.user_groups || []).filter(el => el !== user_group);
});
},

View file

@ -15,6 +15,7 @@ class MentionBlot extends Embed {
node.dataset.id = data.id;
node.dataset.value = data.value;
node.dataset.denotationChar = data.denotationChar;
node.dataset.isGroup = data.isGroup;
if (data.link) {
node.dataset.link = data.link;
}
@ -27,6 +28,7 @@ class MentionBlot extends Embed {
value: domNode.dataset.value,
link: domNode.dataset.link || null,
denotationChar: domNode.dataset.denotationChar,
isGroup: domNode.dataset.isGroup,
};
}
}

View file

@ -149,6 +149,7 @@ class Mention {
this.mentionList.childNodes[this.itemIndex].dataset.value,
link: itemLink || null,
denotationChar: this.mentionList.childNodes[this.itemIndex].dataset.denotationChar,
isGroup: this.mentionList.childNodes[this.itemIndex].dataset.isGroup,
};
}
@ -197,6 +198,7 @@ class Mention {
li.dataset.index = i;
li.dataset.id = data[i].id;
li.dataset.value = data[i].value;
li.dataset.isGroup = Boolean(data[i].is_group);
li.dataset.denotationChar = mentionChar;
if (data[i].link) {
li.dataset.link = data[i].link;

View file

@ -45,9 +45,12 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({
} else {
// no column header, map to the existing visible columns
const visible_columns = grid_rows[0].get_visible_columns();
let target_column_matched = false;
visible_columns.forEach(column => {
if (column.fieldname === $(e.target).data('fieldname')) {
// consider all columns after the target column.
if (target_column_matched || column.fieldname === $(e.target).data('fieldname')) {
fieldnames.push(column.fieldname);
target_column_matched = true;
}
});
}

View file

@ -535,14 +535,14 @@ frappe.ui.form.Dashboard = class FormDashboard {
render_graph(args) {
this.chart_area.show();
this.chart_area.body.empty();
$.extend(args, {
$.extend({
type: 'line',
colors: ['green'],
truncateLegends: 1,
axisOptions: {
shortenYAxisNumbers: 1
}
});
}, args);
this.show();
this.chart = new frappe.Chart('.form-graph', args);

View file

@ -129,6 +129,7 @@ class FormTimeline extends BaseTimeline {
prepare_timeline_contents() {
this.timeline_items.push(...this.get_communication_timeline_contents());
this.timeline_items.push(...this.get_auto_messages_timeline_contents());
this.timeline_items.push(...this.get_comment_timeline_contents());
if (!this.only_communication) {
this.timeline_items.push(...this.get_view_timeline_contents());
@ -181,7 +182,7 @@ class FormTimeline extends BaseTimeline {
return communication_timeline_contents;
}
get_communication_timeline_content(doc) {
get_communication_timeline_content(doc, allow_reply=true) {
doc._url = frappe.utils.get_form_link("Communication", doc.name);
this.set_communication_doc_status(doc);
if (doc.attachments && typeof doc.attachments === "string") {
@ -189,8 +190,10 @@ class FormTimeline extends BaseTimeline {
}
doc.owner = doc.sender;
doc.user_full_name = doc.sender_full_name;
let communication_content = $(frappe.render_template('timeline_message_box', { doc }));
this.setup_reply(communication_content, doc);
let communication_content = $(frappe.render_template('timeline_message_box', { doc }));
if (allow_reply) {
this.setup_reply(communication_content, doc);
}
return communication_content;
}
@ -209,6 +212,22 @@ class FormTimeline extends BaseTimeline {
doc._doc_status_indicator = indicator_color;
}
get_auto_messages_timeline_contents() {
let auto_messages_timeline_contents = [];
(this.doc_info.automated_messages|| []).forEach(message => {
auto_messages_timeline_contents.push({
icon: 'notification',
icon_size: 'sm',
creation: message.creation,
is_card: true,
content: this.get_communication_timeline_content(message, false),
doctype: "Communication",
name: message.name
});
});
return auto_messages_timeline_contents;
}
get_comment_timeline_contents() {
let comment_timeline_contents = [];
(this.doc_info.comments || []).forEach(comment => {

View file

@ -34,7 +34,6 @@ frappe.ui.form.Form = class FrappeForm {
this.grids = [];
this.cscript = new frappe.ui.form.Controller({ frm: this });
this.events = {};
this.pformat = {};
this.fetch_dict = {};
this.parent = parent;
this.doctype_layout = frappe.get_doc('DocType Layout', doctype_layout_name);
@ -1144,10 +1143,6 @@ frappe.ui.form.Form = class FrappeForm {
this.page.remove_inner_button(label, group);
}
set_print_heading(txt) {
this.pformat[this.docname] = txt;
}
scroll_to_element() {
if (frappe.route_options && frappe.route_options.scroll_to) {
var scroll_to = frappe.route_options.scroll_to;

View file

@ -901,4 +901,21 @@ export default class Grid {
// hide all custom buttons
this.grid_buttons.find('.btn-custom').addClass('hidden');
}
update_docfield_property(fieldname, property, value) {
// update the docfield of each row
for (let row of this.grid_rows) {
let docfield = row.docfields.find(d => d.fieldname === fieldname);
if (docfield) {
docfield[property] = value;
} else {
throw `field ${fieldname} not found`;
}
}
// update the parent too (for new rows)
this.docfields.find(d => d.fieldname === fieldname)[property] = value;
this.refresh();
}
}

View file

@ -1,8 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.ui.form.Attachments = Class.extend({
init: function(opts) {
$.extend(this, opts);
@ -84,17 +82,9 @@ frappe.ui.form.Attachments = Class.extend({
};
}
let icon;
// REDESIGN-TODO: set icon using frappe.utils.icon
if (attachment.is_private) {
icon = `<div><svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07685 1.45015H8.02155C7.13255 1.44199 6.2766 1.78689 5.64159 2.40919C5.00596 3.0321 4.64377 3.88196 4.63464 4.77188L4.63462 4.77188V4.77701V5.12157H3.75C2.64543 5.12157 1.75 6.017 1.75 7.12157V12.5132C1.75 13.6177 2.64543 14.5132 3.75 14.5132H12.2885C13.393 14.5132 14.2885 13.6177 14.2885 12.5132V7.12157C14.2885 6.017 13.393 5.12157 12.2885 5.12157H11.4037V4.83708C11.4119 3.94809 11.067 3.09213 10.4447 2.45713C9.82175 1.8215 8.97189 1.4593 8.08198 1.45018L8.08198 1.45015H8.07685ZM10.4037 5.12157V4.8347V4.82972L10.4037 4.82972C10.4099 4.20495 10.1678 3.60329 9.73045 3.15705C9.29371 2.7114 8.69805 2.4572 8.07417 2.45015H8.01916H8.01418L8.01419 2.45013C7.38942 2.44391 6.78776 2.68609 6.34152 3.12341C5.89586 3.56015 5.64166 4.15581 5.63462 4.77969V5.12157H10.4037ZM3.75 6.12157C3.19772 6.12157 2.75 6.56929 2.75 7.12157V12.5132C2.75 13.0655 3.19772 13.5132 3.75 13.5132H12.2885C12.8407 13.5132 13.2885 13.0655 13.2885 12.5132V7.12157C13.2885 6.56929 12.8407 6.12157 12.2885 6.12157H3.75ZM8.01936 10.3908C8.33605 10.3908 8.59279 10.134 8.59279 9.81734C8.59279 9.50064 8.33605 9.24391 8.01936 9.24391C7.70266 9.24391 7.44593 9.50064 7.44593 9.81734C7.44593 10.134 7.70266 10.3908 8.01936 10.3908ZM9.59279 9.81734C9.59279 10.6863 8.88834 11.3908 8.01936 11.3908C7.15038 11.3908 6.44593 10.6863 6.44593 9.81734C6.44593 8.94836 7.15038 8.24391 8.01936 8.24391C8.88834 8.24391 9.59279 8.94836 9.59279 9.81734Z" fill="currentColor"/>
</svg></div>`;
} else {
icon = `<div><svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07685 1.45014H8.02155C7.13255 1.44198 6.2766 1.78687 5.64159 2.40918C5.00596 3.03209 4.64377 3.88195 4.63464 4.77187L4.63462 4.77187V4.777V5.12156H3.75C2.64543 5.12156 1.75 6.01699 1.75 7.12156V12.5132C1.75 13.6177 2.64543 14.5132 3.75 14.5132H12.2885C13.393 14.5132 14.2885 13.6177 14.2885 12.5132V7.12156C14.2885 6.01699 13.393 5.12156 12.2885 5.12156H5.63462V4.77968C5.64166 4.1558 5.89586 3.56014 6.34152 3.12339C6.78776 2.68608 7.38942 2.4439 8.01419 2.45012L8.01418 2.45014H8.01916H8.07417C8.69805 2.45718 9.29371 2.71138 9.73045 3.15704C9.92373 3.35427 10.2403 3.35746 10.4375 3.16418C10.6347 2.9709 10.6379 2.65434 10.4447 2.45711C9.82175 1.82149 8.97189 1.45929 8.08198 1.45017L8.08198 1.45014H8.07685ZM3.75 6.12156C3.19772 6.12156 2.75 6.56927 2.75 7.12156V12.5132C2.75 13.0655 3.19772 13.5132 3.75 13.5132H12.2885C12.8407 13.5132 13.2885 13.0655 13.2885 12.5132V7.12156C13.2885 6.56927 12.8407 6.12156 12.2885 6.12156H3.75ZM8.01936 10.3908C8.33605 10.3908 8.59279 10.134 8.59279 9.81732C8.59279 9.50063 8.33605 9.2439 8.01936 9.2439C7.70266 9.2439 7.44593 9.50063 7.44593 9.81732C7.44593 10.134 7.70266 10.3908 8.01936 10.3908ZM9.59279 9.81732C9.59279 10.6863 8.88834 11.3908 8.01936 11.3908C7.15038 11.3908 6.44593 10.6863 6.44593 9.81732C6.44593 8.94835 7.15038 8.2439 8.01936 8.2439C8.88834 8.2439 9.59279 8.94835 9.59279 9.81732Z" fill="currentColor"/>
</svg></div>`;
}
const icon = `<a href="/app/file/${fileid}">
${frappe.utils.icon(attachment.is_private ? 'lock' : 'unlock', 'sm ml-0')}
</a>`;
$(`<li class="attachment-row">`)
.append(frappe.get_data_pill(

View file

@ -1,10 +0,0 @@
<li class="attachment-row flex align-center">
<a class="close">&times;</a>
<a href="{{ file_path }}">
<i class="{{ icon }} fa-fw text-warning"></i>
</a>
<a href="{{ file_url }}" target="_blank" title="{{ file_name }}" class="ellipsis" style="max-width: calc(100% - 43px);">
<span>{{ file_name }}</span>
</a>
</li>

View file

@ -1,7 +1,32 @@
<div class="timeline-message-box" data-communication-type="{{ doc.communication_type }}">
<span class="flex justify-between">
<span class="text-color flex">
{% if (doc.comment_type && doc.comment_type == "Comment") { %}
{% if (doc.communication_type && doc.communication_type == "Automated Message") { %}
<span>
<!-- Display maximum of 3 users-->
{{ __("Notification sent to") }}
{% var recipients = (doc.recipients && doc.recipients.split(",")) || [] %}
{% var cc = (doc.cc && doc.cc.split(",")) || [] %}
{% var bcc = (doc.bcc && doc.bcc.split(",")) || [] %}
{% var emails = recipients.concat(cc, bcc) %}
{% var display_emails_len = Math.min(emails.length, 3) %}
{% for (var i=0, len=display_emails_len; i<len; i++) { var email = emails[i]; %}
{{ frappe.user_info(email).fullname || email }}
{% if (len > i+1) { %}
{{ "," }}
{% } %}
{% } %}
{% if (emails.length > display_emails_len) { %}
{{ "..." }}
{% } %}
<div class="text-muted">
{{ comment_when(doc.creation) }}
</div>
</span>
{% } else if (doc.comment_type && doc.comment_type == "Comment") { %}
<span>
{{ doc.user_full_name || frappe.user.full_name(doc.owner) }} {{ __("commented") }}
<span class="text-muted margin-left">
@ -64,4 +89,4 @@
{% }); %}
</div>
{% } %}
</div>
</div>

View file

@ -938,7 +938,7 @@ Object.assign(frappe.utils, {
});
},
is_rtl(lang=null) {
return ["ar", "he", "fa"].includes(lang || frappe.boot.lang);
return ["ar", "he", "fa", "ps"].includes(lang || frappe.boot.lang);
},
bind_actions_with_object($el, object) {
// remove previously bound event
@ -1285,6 +1285,16 @@ Object.assign(frappe.utils, {
value: frappe.boot.user_info[user].fullname,
};
});
frappe.boot.user_groups && frappe.boot.user_groups.map(group => {
names_for_mentions.push({
id: group,
value: group,
is_group: true,
link: frappe.utils.get_form_link('User Group', group)
});
});
return names_for_mentions;
},
print(doctype, docname, print_format, letterhead, lang_code) {

View file

@ -603,7 +603,7 @@ frappe.views.CommunicationComposer = Class.extend({
},
delete_saved_draft() {
if (this.dialog) {
if (this.dialog && this.frm) {
localforage.removeItem(this.frm.doctype + this.frm.docname).catch(e => {
if (e) {
// silently fail

View file

@ -119,7 +119,10 @@
border: 1px solid var(--border-color);
padding: 2px 5px;
font-size: var(--text-sm);
background-color: var(--fg-color);
background-color: var(--user-mention-bg-color);
a[href] {
text-decoration: none;
}
}
// table
@ -174,7 +177,7 @@
.ql-editor.read-mode {
padding: 0;
.mention {
background-color: var(--control-bg);
--user-mention-bg-color: var(--control-bg);
}
}
@ -190,4 +193,8 @@
.mention>span {
margin: 0 3px;
}
}
.mention[data-is-group="true"] {
background-color: var(--group-mention-bg-color);
}

View file

@ -59,6 +59,10 @@ $input-height: 28px !default;
--timeline-content-max-width: 700px;
--timeline-left-padding: calc(var(--padding-xl) + var(--timeline-item-icon-size) / 2);
// mentions
--user-mention-bg-color: var(--fg-color);
--group-mention-bg-color: var(--bg-purple);
// skeleton
--skeleton-bg: var(--gray-100);

View file

@ -99,7 +99,7 @@
.ql-editor {
color: var(--text-on-gray);
&.read-mode {
span,
span:not(.mention),
p,
u,
strong {

View file

@ -77,6 +77,7 @@ $threshold: 34;
}
}
.document-email-link-container {
@extend .ellipsis;
position: relative;
padding: var(--padding-sm);
font-size: var(--text-sm);
@ -141,4 +142,4 @@ $threshold: 34;
--icon-stroke: var(--text-color);
}
}
}
}

View file

@ -90,6 +90,13 @@
margin: 2rem 0;
}
@media (max-width: map-get($grid-breakpoints, "lg")) {
.page-content-wrapper .container {
padding-left: 1rem;
padding-right: 1rem;
}
}
.breadcrumb-container {
margin-top: 1rem;
padding-top: 0.25rem;

View file

@ -1,3 +1,15 @@
.navbar {
padding-left: 0;
padding-right: 0;
}
@media (max-width: map-get($grid-breakpoints, "lg")) {
.navbar {
padding-left: 1rem;
padding-right: 1rem;
}
}
.navbar-light {
border-bottom: 1px solid $border-color;
background: $navbar-bg;
@ -96,4 +108,4 @@
@extend .ellipsis;
max-width: 100%;
vertical-align: middle;
}
}

View file

@ -5,7 +5,16 @@
color: var(--text-color);
}
.form-section {
.section-head {
font-weight: bold;
font-size: var(--text-xl);
padding: var(--padding-md) 0;
}
}
.form-column {
padding: 0 var(--padding-md);
&:first-child {
padding-left: 0;
}

View file

@ -56,6 +56,8 @@
}
window.dev_server = {{ dev_server }};
window.socketio_port = {{ (frappe.socketio_port or 'null') }};
window.show_language_picker = {{ show_language_picker }};
window.is_chat_enabled = {{ chat_enable }};
</script>
</head>
<body frappe-session-status="{{ 'logged-in' if frappe.session.user != 'Guest' else 'logged-out'}}" data-path="{{ path | e }}" {%- if template and template.endswith('.md') %} frappe-content-type="markdown" {% endif -%} class="{{ body_class or ''}}">

View file

@ -21,5 +21,8 @@
<div class="collapse navbar-collapse" id="navbarSupportedContent">
{% include "templates/includes/navbar/navbar_items.html" %}
</div>
<div class="form-group mb-0 hide" id="language-switcher">
<select class="form-control"></select>
</div>
</div>
</nav>

View file

@ -7,7 +7,7 @@
<li class="nav-item dropdown {% if submenu %} dropdown-submenu {% endif %}">
<a class="nav-link dropdown-toggle" href="#" id="{{ dropdown_id }}" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ item.label }}
{{ _(item.label) }}
</a>
<ul class="dropdown-menu" aria-labelledby="{{ dropdown_id }}">
{% for child in item.child_items %}
@ -20,7 +20,7 @@
<li class="dropdown {% if submenu %} dropdown-submenu {% endif %}">
<a class="dropdown-item dropdown-toggle" href="#" id="{{ dropdown_id }}" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ item.label }}
{{ _(item.label) }}
</a>
<ul class="dropdown-menu" aria-labelledby="{{ dropdown_id }}">
{% for child in item.child_items %}
@ -36,13 +36,13 @@
<li class="nav-item">
<a class="nav-link" href="{{ (item.url or '')|abs_url }}"
{% if item.open_in_new_tab %} target="_blank" {% endif %}>
{{ item.label }}
{{ _(item.label) }}
</a>
</li>
{% else %}
<a class="dropdown-item" href="{{ (item.url or '') | abs_url }}"
{% if item.open_in_new_tab %} target="_blank" {% endif %}>
{{ item.label }}
{{ _(item.label) }}
</a>
{% endif %}

View file

@ -26,7 +26,7 @@ def get_context(context):
context['amount'] = fmt_money(amount=context['amount'], currency=context['currency'])
if frappe.db.get_value(context.reference_doctype, context.reference_docname, "is_a_subscription"):
if is_a_subscription(context.reference_doctype, context.reference_docname):
payment_plan = frappe.db.get_value(context.reference_doctype, context.reference_docname, "payment_plan")
recurrence = frappe.db.get_value("Payment Plan", payment_plan, "recurrence")
@ -60,7 +60,7 @@ def make_payment(stripe_token_id, data, reference_doctype=None, reference_docnam
gateway_controller = get_gateway_controller(reference_doctype,reference_docname)
if frappe.db.get_value(reference_doctype, reference_docname, 'is_a_subscription'):
if is_a_subscription(reference_doctype, reference_docname):
reference = frappe.get_doc(reference_doctype, reference_docname)
data = reference.create_subscription("stripe", gateway_controller, data)
else:
@ -68,3 +68,8 @@ def make_payment(stripe_token_id, data, reference_doctype=None, reference_docnam
frappe.db.commit()
return data
def is_a_subscription(reference_doctype, reference_docname):
if not frappe.get_meta(reference_doctype).has_field('is_a_subscription'):
return False
return frappe.db.get_value(reference_doctype, reference_docname, "is_a_subscription")

View file

@ -13,6 +13,7 @@ from frappe.permissions import (add_user_permission, remove_user_permission,
from frappe.core.page.permission_manager.permission_manager import update, reset
from frappe.test_runner import make_test_records_for_doctype
from frappe.core.doctype.user_permission.user_permission import clear_user_permissions
from frappe.desk.form.load import getdoc
test_dependencies = ['Blogger', 'Blog Post', "User", "Contact", "Salutation"]
@ -30,6 +31,10 @@ class TestPermissions(unittest.TestCase):
user = frappe.get_doc("User", "test3@example.com")
user.add_roles("Sales User")
user = frappe.get_doc("User", "testperm@example.com")
user.add_roles("Website Manager")
frappe.flags.permission_user_setup_done = True
reset('Blogger')
@ -464,6 +469,74 @@ class TestPermissions(unittest.TestCase):
# delete the created doc
frappe.delete_doc('Blog Post', '-test-blog-post-title')
def test_if_owner_permission_on_getdoc(self):
update('Blog Post', 'Blogger', 0, 'if_owner', 1)
update('Blog Post', 'Blogger', 0, 'read', 1)
update('Blog Post', 'Blogger', 0, 'write', 1)
update('Blog Post', 'Blogger', 0, 'delete', 1)
frappe.clear_cache(doctype="Blog Post")
frappe.set_user("test1@example.com")
doc = frappe.get_doc({
"doctype": "Blog Post",
"blog_category": "-test-blog-category",
"blogger": "_Test Blogger 1",
"title": "_Test Blog Post Title New",
"content": "_Test Blog Post Content"
})
doc.insert()
getdoc('Blog Post', doc.name)
doclist = [d.name for d in frappe.response.docs]
self.assertTrue(doc.name in doclist)
frappe.set_user("test2@example.com")
self.assertRaises(frappe.PermissionError, getdoc, 'Blog Post', doc.name)
def test_if_owner_permission_on_delete(self):
update('Blog Post', 'Blogger', 0, 'if_owner', 1)
update('Blog Post', 'Blogger', 0, 'read', 1)
update('Blog Post', 'Blogger', 0, 'write', 1)
update('Blog Post', 'Blogger', 0, 'delete', 1)
# Remove delete perm
update('Blog Post', 'Website Manager', 0, 'delete', 0)
frappe.clear_cache(doctype="Blog Post")
frappe.set_user("test2@example.com")
doc = frappe.get_doc({
"doctype": "Blog Post",
"blog_category": "-test-blog-category",
"blogger": "_Test Blogger 1",
"title": "_Test Blog Post Title New 1",
"content": "_Test Blog Post Content"
})
doc.insert()
getdoc('Blog Post', doc.name)
doclist = [d.name for d in frappe.response.docs]
self.assertTrue(doc.name in doclist)
frappe.set_user("testperm@example.com")
# Website Manager able to read
getdoc('Blog Post', doc.name)
doclist = [d.name for d in frappe.response.docs]
self.assertTrue(doc.name in doclist)
# Website Manager should not be able to delete
self.assertRaises(frappe.PermissionError, frappe.delete_doc, 'Blog Post', doc.name)
frappe.set_user("test2@example.com")
frappe.delete_doc('Blog Post', '-test-blog-post-title-new-1')
update('Blog Post', 'Website Manager', 0, 'delete', 1)
def test_clear_user_permissions(self):
current_user = frappe.session.user
frappe.set_user('Administrator')

View file

@ -21,6 +21,11 @@ import itertools, operator
def guess_language(lang_list=None):
"""Set `frappe.local.lang` from HTTP headers at beginning of request"""
user_preferred_language = frappe.request.cookies.get('preferred_language')
is_guest_user = not frappe.session.user or frappe.session.user == 'Guest'
if is_guest_user and user_preferred_language:
return user_preferred_language
lang_codes = frappe.request.accept_languages.values()
if not lang_codes:
return frappe.local.lang
@ -77,14 +82,6 @@ def set_default_language(lang):
frappe.db.set_default("lang", lang)
frappe.local.lang = lang
def get_all_languages():
"""Returns all language codes ar, ch etc"""
def _get():
if not frappe.db:
frappe.connect()
return frappe.db.sql_list('select name from tabLanguage')
return frappe.cache().get_value('languages', _get)
def get_lang_dict():
"""Returns all languages in dict format, full name is the key e.g. `{"english":"en"}`"""
return dict(frappe.db.sql('select language_name, name from tabLanguage'))
@ -112,6 +109,13 @@ def get_dict(fortype, name=None):
elif fortype=="jsfile":
messages = get_messages_from_file(name)
elif fortype=="boot":
messages = []
apps = frappe.get_all_apps(True)
for app in apps:
messages.extend(get_server_messages(app))
messages = deduplicate_messages(messages)
messages += frappe.db.sql("""select "navbar", item_label from `tabNavbar Item` where item_label is not null""")
messages = get_messages_from_include_files()
messages += frappe.db.sql("select 'Print Format:', name from `tabPrint Format`")
messages += frappe.db.sql("select 'DocType:', name from tabDocType")
@ -244,6 +248,8 @@ def get_translation_dict_from_file(path, lang, app):
return translation_map
def get_user_translations(lang):
if not frappe.db:
frappe.connect()
out = frappe.cache().hget('lang_user_translations', lang)
if out is None:
out = {}
@ -813,3 +819,24 @@ def get_contribution_status(message_id):
def get_translator_url():
return frappe.get_hooks()['translator_url'][0]
@frappe.whitelist(allow_guest=True)
def get_all_languages(with_language_name=False):
"""Returns all language codes ar, ch etc"""
def get_language_codes():
return frappe.db.sql_list('select name from tabLanguage')
def get_all_language_with_name():
return frappe.db.get_all('language', ['language_code', 'language_name'])
if not frappe.db:
frappe.connect()
if with_language_name:
return frappe.cache().get_value('languages_with_name', get_all_language_with_name)
else:
return frappe.cache().get_value('languages', get_language_codes)
@frappe.whitelist(allow_guest=True)
def set_preferred_language_cookie(preferred_language):
frappe.local.cookie_manager.set_cookie("preferred_language", preferred_language)

View file

@ -216,7 +216,7 @@ def get_traceback():
def log(event, details):
frappe.logger().info(details)
def dict_to_str(args, sep='&'):
def dict_to_str(args, sep = '&'):
"""
Converts a dictionary to URL
"""
@ -225,6 +225,13 @@ def dict_to_str(args, sep='&'):
t.append(str(k)+'='+quote(str(args[k] or '')))
return sep.join(t)
def list_to_str(seq, sep = ', '):
"""Convert a sequence into a string using seperator.
Same as str.join, but does type conversion and strip extra spaces.
"""
return sep.join(map(str.strip, map(str, seq)))
# Get Defaults
# ==============================================================================

View file

@ -177,7 +177,7 @@ acceptable_attributes = [
'data-value', 'role', 'frameborder', 'allowfullscreen', 'spellcheck',
'data-mode', 'data-gramm', 'data-placeholder', 'data-comment',
'data-id', 'data-denotation-char', 'itemprop', 'itemscope',
'itemtype', 'itemid', 'itemref', 'datetime'
'itemtype', 'itemid', 'itemref', 'datetime', 'data-is-group'
]
mathml_attributes = [

View file

@ -12,6 +12,7 @@ from frappe.modules import scrub
from frappe.www.printview import get_visible_columns
import frappe.exceptions
import frappe.integrations.utils
from frappe.frappeclient import FrappeClient
class ServerScriptNotEnabled(frappe.PermissionError):
pass
@ -104,8 +105,10 @@ def get_safe_globals():
make_post_request = frappe.integrations.utils.make_post_request,
socketio_port=frappe.conf.socketio_port,
get_hooks=frappe.get_hooks,
sanitize_html=frappe.utils.sanitize_html
sanitize_html=frappe.utils.sanitize_html,
log_error=frappe.log_error
),
FrappeClient=FrappeClient,
style=frappe._dict(
border_color='#d1d8dd'
),
@ -297,4 +300,4 @@ VALID_UTILS = (
"formatdate",
"get_user_info_for_avatar",
"get_abbr"
)
)

View file

@ -58,7 +58,7 @@ def update_controller_context(context, controller):
ret = module.get_context()
if ret:
context.update(ret)
except (frappe.PermissionError, frappe.DoesNotExistError, frappe.Redirect):
except (frappe.PermissionError, frappe.PageDoesNotExistError, frappe.Redirect):
raise
except:
if not frappe.flags.in_migrate:

View file

@ -20,7 +20,10 @@ frappe.web_form = {
return null;
}
});
frappe.meta.get_docfield("Web Form Field", "fieldname", frm.doc.name).options = [""].concat(fields);
frm.fields_dict.web_form_fields.grid.update_docfield_property(
'fieldname', 'options', fields
);
frappe.meta.get_docfield("Web Form", "amount_field", frm.doc.name).options = [""].concat(currency_fields);
frm.refresh_field("amount_field");
resolve();

View file

@ -215,12 +215,17 @@ def get_context(context):
amount = self.amount
if self.amount_based_on_field:
amount = doc.get(self.amount_field)
from decimal import Decimal
if amount is None or Decimal(amount) <= 0:
return frappe.utils.get_url(self.success_url or self.route)
payment_details = {
"amount": amount,
"title": title,
"description": title,
"reference_doctype": doc.doctype,
"reference_docname": doc.name,
"reference_doctype": "Web Form",
"reference_docname": self.name,
"payer_email": frappe.session.user,
"payer_name": frappe.utils.get_fullname(frappe.session.user),
"order_id": doc.name,

View file

@ -86,14 +86,14 @@
"width": "50%"
},
{
"default": "0",
"default": "1",
"fieldname": "published",
"fieldtype": "Check",
"in_standard_filter": 1,
"label": "Published"
},
{
"default": "1",
"default": "0",
"fieldname": "show_title",
"fieldtype": "Check",
"label": "Show Title"
@ -114,7 +114,7 @@
"label": "Content"
},
{
"default": "Rich Text",
"default": "Page Builder",
"fieldname": "content_type",
"fieldtype": "Select",
"label": "Content Type",
@ -259,7 +259,7 @@
"options": "Web Page Block"
},
{
"default": "0",
"default": "1",
"fieldname": "full_width",
"fieldtype": "Check",
"label": "Full Width"
@ -314,7 +314,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 20,
"modified": "2020-09-21 16:32:53.568573",
"modified": "2021-04-13 10:23:28.681197",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Page",

View file

@ -30,8 +30,9 @@ frappe.ui.form.on('Website Settings', {
},
set_parent_label_options: function(frm) {
frappe.meta.get_docfield("Top Bar Item", "parent_label", frm.docname).options =
frm.events.get_parent_options(frm, "top_bar_items");
frm.fields_dict.top_bar_items.grid.update_docfield_property(
'parent_label', 'options', frm.events.get_parent_options(frm, "top_bar_items")
);
if ($(frm.fields_dict.top_bar_items.grid.wrapper).find(".grid-row-open")) {
frm.fields_dict.top_bar_items.grid.refresh();
@ -39,8 +40,9 @@ frappe.ui.form.on('Website Settings', {
},
set_parent_label_options_footer: function(frm) {
frappe.meta.get_docfield("Top Bar Item", "parent_label", frm.docname).options =
frm.events.get_parent_options(frm, "footer_items");
frm.fields_dict.footer_items.grid.update_docfield_property(
'parent_label', 'options', frm.events.get_parent_options(frm, "top_bar_items")
);
if ($(frm.fields_dict.footer_items.grid.wrapper).find(".grid-row-open")) {
frm.fields_dict.footer_items.grid.refresh();

View file

@ -25,9 +25,11 @@
"set_banner_from_image",
"favicon",
"top_bar",
"navbar_search",
"hide_login",
"top_bar_items",
"hide_login",
"navbar_search",
"show_language_picker",
"navbar_template_section",
"navbar_template",
"navbar_template_values",
"edit_navbar_template_values",
@ -142,6 +144,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "top_bar_items",
"fieldname": "top_bar",
"fieldtype": "Section Break",
"label": "Navbar"
@ -160,6 +163,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "banner_html",
"fieldname": "banner",
"fieldtype": "Section Break",
"label": "Banner"
@ -173,6 +177,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "footer_items",
"fieldname": "footer",
"fieldtype": "Section Break",
"label": "Footer"
@ -407,6 +412,19 @@
"fieldname": "google_analytics_anonymize_ip",
"fieldtype": "Check",
"label": "Google Analytics Anonymize IP"
},
{
"default": "0",
"fieldname": "show_language_picker",
"fieldtype": "Check",
"label": "Show Language Picker"
},
{
"collapsible": 1,
"collapsible_depends_on": "navbar_template",
"fieldname": "navbar_template_section",
"fieldtype": "Section Break",
"label": "Navbar Template"
}
],
"icon": "fa fa-cog",
@ -415,7 +433,7 @@
"issingle": 1,
"links": [],
"max_attachments": 10,
"modified": "2020-09-28 18:47:18.506700",
"modified": "2021-04-14 17:39:56.609771",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Settings",

View file

@ -121,7 +121,8 @@ def get_website_settings(context=None):
"facebook_share", "google_plus_one", "twitter_share", "linked_in_share",
"disable_signup", "hide_footer_signup", "head_html", "title_prefix",
"navbar_template", "footer_template", "navbar_search", "enable_view_tracking",
"footer_logo", "call_to_action", "call_to_action_url"]:
"footer_logo", "call_to_action", "call_to_action_url", "show_language_picker",
"chat_enable"]:
if hasattr(settings, k):
context[k] = settings.get(k)
@ -178,7 +179,3 @@ def get_items(parentfield):
t['child_items'].append(d)
break
return top_items
@frappe.whitelist(allow_guest=True)
def is_chat_enabled():
return bool(frappe.db.get_single_value('Website Settings', 'chat_enable'))

View file

@ -180,7 +180,7 @@ def after_migrate():
the end of every `bench migrate`.
"""
website_theme = frappe.db.get_single_value('Website Settings', 'website_theme')
if website_theme == 'Standard':
if not website_theme or website_theme == 'Standard':
return
doc = frappe.get_doc('Website Theme', website_theme)

View file

@ -376,6 +376,39 @@ $.extend(frappe, {
// Start observing an element
io.observe(el);
});
},
show_language_picker() {
if (frappe.session.user === 'Guest' && window.show_language_picker) {
frappe.call("frappe.translate.get_all_languages", {
with_language_name: true
}).then(res => {
let language_list = res.message;
let language = frappe.get_cookie('preferred_language');
let language_codes = [];
let language_switcher = $("#language-switcher .form-control");
language_list.forEach(language_doc => {
language_codes.push(language_doc.language_code);
language_switcher
.append(
$("<option></option>")
.attr("value", language_doc.language_code)
.text(language_doc.language_name)
);
});
$("#language-switcher").removeClass('hide');
language = language || (language_codes.includes(navigator.language) ? navigator.language : 'en');
language_switcher.val(language);
document.documentElement.lang = language;
language_switcher.change(() => {
let lang = language_switcher.val();
frappe.call("frappe.translate.set_preferred_language_cookie", {
"preferred_language": lang
}).then(() => {
window.location.reload();
});
});
});
}
}
});
@ -599,17 +632,13 @@ $(document).on("page-change", function() {
frappe.ready(function() {
frappe.call({
method: 'frappe.website.doctype.website_settings.website_settings.is_chat_enabled',
callback: (r) => {
if (r.message) {
frappe.require(['/assets/js/moment-bundle.min.js', "/assets/css/frappe-chat-web.css", "/assets/frappe/js/lib/socket.io.min.js"], () => {
frappe.require('/assets/js/chat.js', () => {
frappe.chat.setup();
});
});
}
}
});
frappe.show_language_picker();
if (window.is_chat_enabled) {
frappe.require(['/assets/js/moment-bundle.min.js', "/assets/css/frappe-chat-web.css", "/assets/frappe/js/lib/socket.io.min.js"], () => {
frappe.require('/assets/js/chat.js', () => {
frappe.chat.setup();
});
});
}
frappe.socketio.init(window.socketio_port);
});

View file

@ -1,4 +1,6 @@
{%- if title -%}
<h2 class="section-title">{{ title }}</h2>
{%- endif -%}
{%- if subtitle -%}
<p class="section-description">{{ subtitle }}</p>

View file

@ -44,7 +44,9 @@ frappe.ui.form.on("Workflow", {
const get_field_method = 'frappe.workflow.doctype.workflow.workflow.get_fieldnames_for';
frappe.xcall(get_field_method, { doctype: doc.document_type })
.then(resp => {
frappe.meta.get_docfield("Workflow Document State", "update_field", frm.doc.name).options = [""].concat(resp);
frm.fields_dict.states.grid.update_docfield_property(
'update_field', 'options', [""].concat(resp)
);
})
}
},

View file

@ -48,7 +48,7 @@ def get_context(context):
"css": get_print_style(frappe.form_dict.style, print_format),
"comment": frappe.session.user,
"title": doc.get(meta.title_field) if meta.title_field else doc.name,
"has_rtl": True if frappe.local.lang in ["ar", "he", "fa"] else False
"has_rtl": True if frappe.local.lang in ["ar", "he", "fa", "ps"] else False
}
def get_print_format_doc(print_format_name, meta):