Merge branch 'develop' into fix-backup
This commit is contained in:
commit
d8ec0cd4d1
90 changed files with 972 additions and 214 deletions
|
|
@ -34,6 +34,7 @@ if PY2:
|
|||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
__version__ = '13.0.0-dev'
|
||||
|
||||
__title__ = "Frappe Framework"
|
||||
|
||||
local = Local()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
0
frappe/core/doctype/user_group/__init__.py
Normal file
0
frappe/core/doctype/user_group/__init__.py
Normal file
10
frappe/core/doctype/user_group/test_user_group.py
Normal file
10
frappe/core/doctype/user_group/test_user_group.py
Normal 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
|
||||
8
frappe/core/doctype/user_group/user_group.js
Normal file
8
frappe/core/doctype/user_group/user_group.js
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
||||
48
frappe/core/doctype/user_group/user_group.json
Normal file
48
frappe/core/doctype/user_group/user_group.json
Normal 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
|
||||
}
|
||||
15
frappe/core/doctype/user_group/user_group.py
Normal file
15
frappe/core/doctype/user_group/user_group.py
Normal 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)
|
||||
0
frappe/core/doctype/user_group_member/__init__.py
Normal file
0
frappe/core/doctype/user_group_member/__init__.py
Normal 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
|
||||
|
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
||||
32
frappe/core/doctype/user_group_member/user_group_member.json
Normal file
32
frappe/core/doctype/user_group_member/user_group_member.json
Normal 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
|
||||
}
|
||||
10
frappe/core/doctype/user_group_member/user_group_member.py
Normal file
10
frappe/core/doctype/user_group_member/user_group_member.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
22
frappe/patches/v13_0/queryreport_columns.py
Normal file
22
frappe/patches/v13_0/queryreport_columns.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
<li class="attachment-row flex align-center">
|
||||
<a class="close">×</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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@
|
|||
.ql-editor {
|
||||
color: var(--text-on-gray);
|
||||
&.read-mode {
|
||||
span,
|
||||
span:not(.mention),
|
||||
p,
|
||||
u,
|
||||
strong {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ''}}">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# ==============================================================================
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
{%- if title -%}
|
||||
<h2 class="section-title">{{ title }}</h2>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if subtitle -%}
|
||||
<p class="section-description">{{ subtitle }}</p>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue