Merge branch 'develop' of https://github.com/frappe/frappe into if_owner_per_check_url

This commit is contained in:
Deepesh Garg 2021-04-12 21:24:19 +05:30
commit a0fa3d0fd5
115 changed files with 2080 additions and 813 deletions

View file

@ -42,18 +42,33 @@ context('Form', () => {
it('validates behaviour of Data options validations in child table', () => {
// test email validations for set_invalid controller
let website_input = 'website.in';
let valid_email = 'user@email.com';
let expectBackgroundColor = 'rgb(255, 245, 245)';
cy.visit('/app/contact/new');
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
cy.get('@table').find('button.grid-add-row').click();
cy.get('.grid-body .rows [data-fieldname="email_id"]').click();
cy.get('@table').find('input.input-with-feedback.form-control').as('email_input');
cy.get('@email_input').type(website_input, { waitForAnimations: false });
cy.get('@table').find('button.grid-add-row').click();
cy.get('@table').find('[data-idx="1"]').as('row1');
cy.get('@table').find('[data-idx="2"]').as('row2');
cy.get('@row1').click();
cy.get('@row1').find('input.input-with-feedback.form-control').as('email_input1');
cy.get('@email_input1').type(website_input, { waitForAnimations: false });
cy.fill_field('company_name', 'Test Company');
cy.get('@email_input').should($div => {
cy.get('@row2').click();
cy.get('@row2').find('input.input-with-feedback.form-control').as('email_input2');
cy.get('@email_input2').type(valid_email, { waitForAnimations: false });
cy.get('@row1').click();
cy.get('@email_input1').should($div => {
const style = window.getComputedStyle($div[0]);
expect(style.backgroundColor).to.equal(expectBackgroundColor);
});
cy.get('@email_input1').should('have.class', 'invalid');
cy.get('@row2').click();
cy.get('@email_input2').should('not.have.class', 'invalid');
});
});

View file

@ -196,7 +196,7 @@ def make_auto_repeat(**args):
return doc
def create_submittable_doctype(doctype):
def create_submittable_doctype(doctype, submit_perms=1):
if frappe.db.exists('DocType', doctype):
return
else:
@ -217,9 +217,9 @@ def create_submittable_doctype(doctype):
'write': 1,
'create': 1,
'delete': 1,
'submit': 1,
'cancel': 1,
'amend': 1
'submit': submit_perms,
'cancel': submit_perms,
'amend': submit_perms
}]
}).insert()

View file

@ -0,0 +1,54 @@
# Version 13.0.0 Release Notes
## Highlights
- Re-branded UI 💎 ✨🎊 ([#12277](https://github.com/frappe/frappe/pull/12277))
- New Page Builder in Web Page ([#10035](https://github.com/frappe/frappe/pull/10035))
- Customizable desk ([#9617](https://github.com/frappe/frappe/pull/9617))
- Custom Dashboard for DocTypes ([#9872](https://github.com/frappe/frappe/pull/9872))
- Widgets to make dashboards ([#9693](https://github.com/frappe/frappe/pull/9693))
- Events Streaming ([#8567](https://github.com/frappe/frappe/pull/8567))
- Contextual translation and Translation Tool ([#9636](https://github.com/frappe/frappe/pull/9636))
### Other Features & Enhancements
- Added permission to grant only `Select` access ([#12063](https://github.com/frappe/frappe/pull/12063))
- Add columns and filters for reports via configuration ([#11287](https://github.com/frappe/frappe/pull/11287))
- Configurable Navbar logo and dropdowns ([#11213](https://github.com/frappe/frappe/pull/11213))
- Rule based naming of documents ([#11439](https://github.com/frappe/frappe/pull/11439))
- New routing style, not using hashes, also /desk -> /app ([#11917](https://github.com/frappe/frappe/pull/11917))
- Web Page tracking ([#9959](https://github.com/frappe/frappe/pull/9959))
- Introduced "Yesterday" and "Tomorrow" options for Timespan filter ([12179](https://github.com/frappe/frappe/pull/12179))
- Child table pagination ([#8786](https://github.com/frappe/frappe/pull/8786))
- Introduced Duration Control ([#10248](https://github.com/frappe/frappe/pull/10248))
- Form Tour feature ([#10287](https://github.com/frappe/frappe/pull/10287))
<details>
<summary>More</summary>
- Introduced Map View ([#11202](https://github.com/frappe/frappe/pull/11202))
- Custom JS & CSS support in Web Form ([#9121](https://github.com/frappe/frappe/pull/9121)) ([#9610](https://github.com/frappe/frappe/pull/9610))
- Ability to attach photo from webcam ([#12160](https://github.com/frappe/frappe/pull/12160))
- Added a System Console to help in debugging ([#11306](https://github.com/frappe/frappe/pull/11306))
- Introduced System Settings to automatically delete old Prepared Reports ([#9751](https://github.com/frappe/frappe/pull/9751))
- "Mandatory Depends On" and "Read Only Depends On" option for document fields ([#8820](https://github.com/frappe/frappe/pull/8820))
- Added 2FA for LDAP users ([#10001](https://github.com/frappe/frappe/pull/10001))
- Introduced Help Article Feedback system ([#10260](https://github.com/frappe/frappe/pull/10260))
- Introduced Razorpay client ([#11418](https://github.com/frappe/frappe/pull/11418))
- Rate Limiting ([#10310](https://github.com/frappe/frappe/pull/10310))
- Introduced Log Settings ([#11699](https://github.com/frappe/frappe/pull/11699))
- Enhancements in notifications ([#11398](https://github.com/frappe/frappe/pull/11398)) ([#11409](https://github.com/frappe/frappe/pull/11409))
- Added a field-level permission check for report data ([12163](https://github.com/frappe/frappe/pull/12163))
- Ability to cancel all linked document with a single click ([#8905](https://github.com/frappe/frappe/pull/8905))
- Made checkboxes navigable via tab key ([#11030](https://github.com/frappe/frappe/pull/11030))
- Renamed "Custom Script" to "Client Script" ([#12324](https://github.com/frappe/frappe/pull/12324))
</details>
### Performance
- Faster application load ([#12364](https://github.com/frappe/frappe/pull/12364)) ([#10229](https://github.com/frappe/frappe/pull/10229)) ([#10147](https://github.com/frappe/frappe/pull/10147)) ([#9930](https://github.com/frappe/frappe/pull/9930))
- Theme files will now be compressed to make the website load faster ([#11048](https://github.com/frappe/frappe/pull/11048))
- Confirmation emails will be sent instantly ([#10790](https://github.com/frappe/frappe/pull/10790))
- Faster scheduled job processing ([#9928](https://github.com/frappe/frappe/pull/9928))
- Faster data imports ([#12565](https://github.com/frappe/frappe/pull/12565))
- Faster CLI commands ([#12447](https://github.com/frappe/frappe/pull/12447))

View file

@ -21,7 +21,7 @@ Requests via FrappeClient are also handled here.
@frappe.whitelist()
def get_list(doctype, fields=None, filters=None, order_by=None,
limit_start=None, limit_page_length=20, parent=None):
limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True):
'''Returns a list of records by filters, fields, ordering and limit
:param doctype: DocType of the data to be queried
@ -40,10 +40,11 @@ def get_list(doctype, fields=None, filters=None, order_by=None,
order_by=order_by,
limit_start=limit_start,
limit_page_length=limit_page_length,
debug=debug,
as_list=not as_dict
)
validate_args(args)
return frappe.get_list(**args)
@frappe.whitelist()
@ -103,14 +104,15 @@ 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, limit_page_length=1)
value = get_list(doctype, filters=filters, fields=fields, debug=debug, limit_page_length=1, parent=parent, as_dict=as_dict)
if as_dict:
value = value[0] if value else {}
else:
value = value[0][fieldname]
return value[0] if value else {}
return value
if not value:
return
return value[0] if len(fields) > 1 else value[0][0]
@frappe.whitelist()
def get_single_value(doctype, field):

View file

@ -159,7 +159,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
"""Updates `_comments` property in parent Document with given dict.
:param _comments: Dict of comments."""
if not reference_doctype or not reference_name or frappe.db.get_value("DocType", reference_doctype, "issingle"):
if not reference_doctype or not reference_name or frappe.db.get_value("DocType", reference_doctype, "issingle") or frappe.db.get_value("DocType", reference_doctype, "is_virtual"):
return
try:

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

@ -1,293 +1,110 @@
{
"allow_copy": 0,
"actions": [],
"allow_import": 1,
"allow_rename": 0,
"autoname": "hash",
"beta": 0,
"creation": "2015-02-04 04:33:36.330477",
"custom": 0,
"description": "Internal record of document shares",
"docstatus": 0,
"doctype": "DocType",
"document_type": "System",
"editable_grid": 0,
"engine": "InnoDB",
"field_order": [
"user",
"share_doctype",
"share_name",
"read",
"write",
"share",
"submit",
"everyone",
"notify_by_email"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"unique": 0
"search_index": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "share_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Document Type",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 1,
"set_only_once": 0,
"unique": 0
"search_index": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "share_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Document Name",
"length": 0,
"no_copy": 0,
"options": "share_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 1,
"set_only_once": 0,
"unique": 0
"search_index": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "read",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Read",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"label": "Read"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "write",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Write",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"label": "Write"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "share",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Share",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"label": "Share"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "everyone",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Everyone",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"label": "Everyone"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "notify_by_email",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Notify by email",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"print_hide": 1
},
{
"default": "0",
"fieldname": "submit",
"fieldtype": "Check",
"label": "Submit"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-09-15 15:58:34.126438",
"links": [],
"modified": "2021-04-04 11:38:50.813312",
"modified_by": "Administrator",
"module": "Core",
"name": "DocShare",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 0,
"export": 1,
"if_owner": 0,
"import": 1,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
"track_changes": 1
}

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import get_fullname
from frappe.utils import get_fullname, cint
exclude_from_linked_with = True
@ -15,12 +15,15 @@ class DocShare(Document):
def validate(self):
self.validate_user()
self.check_share_permission()
self.check_is_submittable()
self.cascade_permissions_downwards()
self.get_doc().run_method("validate_share", self)
def cascade_permissions_downwards(self):
if self.share or self.write:
if self.share or self.write or self.submit:
self.read = 1
if self.submit:
self.write = 1
def get_doc(self):
if not getattr(self, "_doc", None):
@ -39,6 +42,11 @@ class DocShare(Document):
frappe.throw(_('You need to have "Share" permission'), frappe.PermissionError)
def check_is_submittable(self):
if self.submit and not cint(frappe.db.get_value("DocType", self.share_doctype, "is_submittable")):
frappe.throw(_("Cannot share {0} with submit permission as the doctype {1} is not submittable").format(
frappe.bold(self.share_name), frappe.bold(self.share_doctype)))
def after_insert(self):
doc = self.get_doc()
owner = get_fullname(self.owner)

View file

@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe
import frappe.share
import unittest
from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype
class TestDocShare(unittest.TestCase):
def setUp(self):
@ -91,3 +92,24 @@ class TestDocShare(unittest.TestCase):
self.assertTrue(self.event.name not in frappe.share.get_shared("Event", self.user))
self.assertTrue(self.event.name not in frappe.share.get_shared("Event", "test1@example.com"))
self.assertTrue(self.event.name not in frappe.share.get_shared("Event", "Guest"))
def test_share_with_submit_perm(self):
doctype = "Test DocShare with Submit"
create_submittable_doctype(doctype, submit_perms=0)
submittable_doc = frappe.get_doc(dict(doctype=doctype, test="test docshare with submit")).insert()
frappe.set_user(self.user)
self.assertFalse(frappe.has_permission(doctype, "submit", user=self.user))
frappe.set_user("Administrator")
frappe.share.add(doctype, submittable_doc.name, self.user, submit=1)
frappe.set_user(self.user)
self.assertTrue(frappe.has_permission(doctype, "submit", doc=submittable_doc.name, user=self.user))
# test cascade
self.assertTrue(frappe.has_permission(doctype, "read", doc=submittable_doc.name, user=self.user))
self.assertTrue(frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user))
frappe.share.remove(doctype, submittable_doc.name, self.user)

View file

@ -7,4 +7,4 @@ from __future__ import unicode_literals
{base_class_import}
class {classname}({base_class}):
pass
{custom_controller}

View file

@ -13,11 +13,21 @@
frappe.ui.form.on('DocType', {
refresh: function(frm) {
frm.set_query('role', 'permissions', function(doc) {
if (doc.custom && frappe.session.user != 'Administrator') {
return {
query: "frappe.core.doctype.role.role.role_query",
filters: [['Role', 'name', '!=', 'All']]
};
}
});
if(frappe.session.user !== "Administrator" || !frappe.boot.developer_mode) {
if(frm.is_new()) {
frm.set_value("custom", 1);
}
frm.toggle_enable("custom", 0);
frm.toggle_enable("is_virtual", 0);
frm.toggle_enable("beta", 0);
}

View file

@ -22,6 +22,7 @@
"track_views",
"custom",
"beta",
"is_virtual",
"fields_section_break",
"fields",
"sb1",
@ -528,6 +529,12 @@
"fieldname": "index_web_pages_for_search",
"fieldtype": "Check",
"label": "Index Web Pages for Search"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
}
],
"icon": "fa fa-bolt",
@ -609,7 +616,7 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2021-02-04 15:10:09.227205",
"modified": "2021-02-17 20:18:06.212232",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -127,6 +127,10 @@ class DocType(Document):
if not frappe.conf.get("developer_mode") and not self.custom:
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError)
if self.is_virtual and self.custom:
frappe.throw(_("Not allowed to create custom Virtual DocType."), CannotCreateStandardDoctypeError)
if frappe.conf.get('developer_mode'):
self.owner = 'Administrator'
self.modified_by = 'Administrator'
@ -1112,6 +1116,21 @@ def validate_permissions(doctype, for_remove=False, alert=False):
if d.get("import") and not isimportable:
frappe.throw(_("{0}: Cannot set import as {1} is not importable").format(get_txt(d), doctype))
def validate_permission_for_all_role(d):
if frappe.session.user == 'Administrator':
return
if doctype.custom:
if d.role == 'All':
frappe.throw(_('Row # {0}: Non administrator user can not set the role {1} to the custom doctype')
.format(d.idx, frappe.bold(_('All'))), title=_('Permissions Error'))
roles = [row.name for row in frappe.get_all('Role', filters={'is_custom': 1})]
if d.role in roles:
frappe.throw(_('Row # {0}: Non administrator user can not set the role {1} to the custom doctype')
.format(d.idx, frappe.bold(_(d.role))), title=_('Permissions Error'))
for d in permissions:
if not d.permlevel:
d.permlevel=0
@ -1123,6 +1142,7 @@ def validate_permissions(doctype, for_remove=False, alert=False):
check_if_importable(d)
check_level_zero_is_set(d)
remove_rights_for_single(d)
validate_permission_for_all_role(d)
def make_module_and_roles(doc, perm_fieldname="permissions"):
"""Make `Module Def` and `Role` records if already not made. Called while installing."""

View file

@ -480,8 +480,19 @@ class TestDocType(unittest.TestCase):
'link_doctype': "User",
'link_fieldname': "a_field_that_does_not_exists"
})
self.assertRaises(InvalidFieldNameError, validate_links_table_fieldnames, doc)
def test_create_virtual_doctype(self):
"""Test virtual DOcTYpe."""
virtual_doc = new_doctype('Test Virtual Doctype')
virtual_doc.is_virtual = 1
virtual_doc.insert()
virtual_doc.save()
doc = frappe.get_doc("DocType", "Test Virtual Doctype")
self.assertEqual(doc.is_virtual, 1)
self.assertFalse(frappe.db.table_exists('Test Virtual Doctype'))
def new_doctype(name, unique=0, depends_on='', fields=None):
doc = frappe.get_doc({

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

@ -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

@ -3,6 +3,8 @@
frappe.ui.form.on('Role', {
refresh: function(frm) {
frm.set_df_property('is_custom', 'read_only', frappe.session.user !== 'Administrator');
frm.add_custom_button("Role Permissions Manager", function() {
frappe.route_options = {"role": frm.doc.name};
frappe.set_route("permission-manager");

View file

@ -25,7 +25,8 @@
"form_settings_section",
"form_sidebar",
"timeline",
"dashboard"
"dashboard",
"is_custom"
],
"fields": [
{
@ -141,13 +142,20 @@
"fieldname": "notifications",
"fieldtype": "Check",
"label": "Notifications"
},
{
"default": "0",
"fieldname": "is_custom",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Custom"
}
],
"icon": "fa fa-bookmark",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-12-03 14:08:38.181035",
"modified": "2021-01-27 10:35:37.638350",
"modified_by": "Administrator",
"module": "Core",
"name": "Role",

View file

@ -33,7 +33,7 @@ class Role(Document):
# set if desk_access is not allowed, unset all desk properties
if self.name == 'Guest':
self.desk_access = 0
if not self.desk_access:
for key in desk_properties:
self.set(key, 0)
@ -53,7 +53,6 @@ class Role(Document):
if user_type != user.user_type:
user.save()
def get_info_based_on_role(role, field='email'):
''' Get information of all users that have been assigned this role '''
users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"},
@ -73,3 +72,15 @@ def get_user_info(users, field='email'):
def get_users(role):
return [d.parent for d in frappe.get_all("Has Role", filters={"role": role, "parenttype": "User"},
fields=["parent"])]
# searches for active employees
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def role_query(doctype, txt, searchfield, start, page_len, filters):
report_filters = [['Role', 'name', 'like', '%{}%'.format(txt)], ['Role', 'is_custom', '=', 0]]
if filters and isinstance(filters, list):
report_filters.extend(filters)
return frappe.get_all('Role', limit_start=start, limit_page_length=page_len,
filters=report_filters, as_list=1)

View file

View file

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

View file

@ -0,0 +1,42 @@
{
"actions": [],
"creation": "2021-03-31 10:06:57.919697",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"test"
],
"fields": [
{
"fieldname": "test",
"fieldtype": "Data",
"label": "Test"
}
],
"index_web_pages_for_search": 1,
"is_virtual": 1,
"links": [],
"modified": "2021-03-31 10:06:57.919697",
"modified_by": "Administrator",
"module": "Core",
"name": "test",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,35 @@
# -*- 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 json
class test(Document):
def db_insert(self):
d = self.get_valid_dict(convert_dates_to_str=True)
with open("data_file.json", "w+") as read_file:
json.dump(d, read_file)
def load_from_db(self):
with open("data_file.json", "r") as read_file:
d = json.load(read_file)
super(Document, self).__init__(d)
def db_update(self):
d = self.get_valid_dict(convert_dates_to_str=True)
with open("data_file.json", "w+") as read_file:
json.dump(d, read_file)
def get_list(self, args):
with open("data_file.json", "r") as read_file:
return [json.load(read_file)]
def get_value(self, fields, filters, **kwargs):
# return []
with open("data_file.json", "r") as read_file:
return [json.load(read_file)]

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 Testtest(unittest.TestCase):
pass

View file

@ -38,6 +38,13 @@
"new_password": "Eastern_43A1W",
"enabled": 1
},
{
"doctype": "User",
"email": "test4@example.com",
"first_name": "_Test4",
"new_password": "Eastern_43A1W",
"enabled": 1
},
{
"doctype": "User",
"email": "testperm@example.com",

View file

@ -247,29 +247,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

@ -59,15 +59,18 @@ frappe.ui.form.on('User', {
onload: function(frm) {
frm.can_edit_roles = has_access_to_edit_user();
if (frm.can_edit_roles && !frm.is_new()) {
if (frm.can_edit_roles && !frm.is_new() && in_list(['System User', 'Website User'], frm.doc.user_type)) {
if (!frm.roles_editor) {
const role_area = $('<div class="role-editor">')
.appendTo(frm.fields_dict.roles_html.wrapper);
frm.roles_editor = new frappe.RoleEditor(role_area, frm, frm.doc.role_profile_name ? 1 : 0);
var module_area = $('<div>')
.appendTo(frm.fields_dict.modules_html.wrapper);
frm.module_editor = new frappe.ModuleEditor(frm, module_area);
if (frm.doc.user_type == 'System User') {
var module_area = $('<div>')
.appendTo(frm.fields_dict.modules_html.wrapper);
frm.module_editor = new frappe.ModuleEditor(frm, module_area);
}
} else {
frm.roles_editor.show();
}
@ -75,7 +78,8 @@ frappe.ui.form.on('User', {
},
refresh: function(frm) {
var doc = frm.doc;
if(!frm.is_new() && !frm.roles_editor && frm.can_edit_roles) {
if (in_list(['System User', 'Website User'], frm.doc.user_type)
&& !frm.is_new() && !frm.roles_editor && frm.can_edit_roles) {
frm.reload_doc();
return;
}
@ -250,15 +254,15 @@ frappe.ui.form.on('User', {
}
});
},
generate_keys: function(frm){
generate_keys: function(frm) {
frappe.call({
method: 'frappe.core.doctype.user.user.generate_keys',
args: {
user: frm.doc.name
},
callback: function(r){
if(r.message){
frappe.msgprint(__("Save API Secret: ") + r.message.api_secret);
callback: function(r) {
if (r.message) {
frappe.msgprint(__("Save API Secret: {0}", [r.message.api_secret]));
}
}
});

View file

@ -191,7 +191,7 @@
"print_hide": 1
},
{
"depends_on": "enabled",
"depends_on": "eval:in_list(['System User', 'Website User'], doc.user_type) && doc.enabled == 1",
"fieldname": "sb1",
"fieldtype": "Section Break",
"label": "Roles",
@ -391,6 +391,7 @@
},
{
"collapsible": 1,
"depends_on": "eval:in_list(['System User'], doc.user_type)",
"fieldname": "sb_allow_modules",
"fieldtype": "Section Break",
"label": "Allow Modules",
@ -453,18 +454,18 @@
"label": "Simultaneous Sessions"
},
{
"bold": 1,
"default": "System User",
"description": "If the user has any role checked, then the user becomes a \"System User\". \"System User\" has access to the desktop",
"fieldname": "user_type",
"fieldtype": "Select",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "User Type",
"oldfieldname": "user_type",
"oldfieldtype": "Select",
"options": "System User\nWebsite User",
"permlevel": 1,
"read_only": 1
"options": "User Type",
"permlevel": 1
},
{
"description": "Allow user to login only after this hour (0-24)",
@ -669,7 +670,7 @@
}
],
"max_attachments": 5,
"modified": "2021-02-01 16:11:06.037543",
"modified": "2021-02-02 16:11:06.037543",
"modified_by": "Administrator",
"module": "Core",
"name": "User",

View file

@ -10,7 +10,8 @@ import frappe.share
import frappe.defaults
import frappe.permissions
from frappe.model.document import Document
from frappe.utils import cint, flt, has_gravatar, escape_html, format_datetime, now_datetime, get_formatted_email, today
from frappe.utils import (cint, flt, has_gravatar, escape_html, format_datetime,
now_datetime, get_formatted_email, today)
from frappe import throw, msgprint, _
from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit
from frappe.desk.notifications import clear_notifications
@ -19,6 +20,7 @@ from frappe.utils.user import get_system_managers
from frappe.website.utils import is_signup_enabled
from frappe.rate_limiter import rate_limit
from frappe.utils.background_jobs import enqueue
from frappe.core.doctype.user_type.user_type import user_linked_with_permission_on_doctype
STANDARD_USERS = ("Guest", "Administrator")
@ -186,11 +188,36 @@ class User(Document):
_update_password(user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions)
def set_system_user(self):
'''Set as System User if any of the given roles has desk_access'''
if self.has_desk_access() or self.name == 'Administrator':
self.user_type = 'System User'
'''For the standard users like admin and guest, the user type is fixed.'''
user_type_mapper = {
'Administrator': 'System User',
'Guest': 'Website User'
}
if self.user_type and not frappe.get_cached_value('User Type', self.user_type, 'is_standard'):
if user_type_mapper.get(self.name):
self.user_type = user_type_mapper.get(self.name)
else:
self.set_roles_and_modules_based_on_user_type()
else:
self.user_type = 'Website User'
'''Set as System User if any of the given roles has desk_access'''
self.user_type = 'System User' if self.has_desk_access() else 'Website User'
def set_roles_and_modules_based_on_user_type(self):
user_type_doc = frappe.get_cached_doc('User Type', self.user_type)
if user_type_doc.role:
self.roles = []
# Check whether User has linked with the 'Apply User Permission On' doctype or not
if user_linked_with_permission_on_doctype(user_type_doc, self.name):
self.append('roles', {
'role': user_type_doc.role
})
frappe.msgprint(_('Role has been set as per the user type {0}')
.format(self.user_type), alert=True)
user_type_doc.update_modules_in_user(self)
def has_desk_access(self):
'''Return true if any of the set roles has desk access'''
@ -877,7 +904,8 @@ def reset_password(user):
def user_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond, get_filters_cond
conditions=[]
user_type_condition = "and user_type = 'System User'"
user_type_condition = "and user_type != 'Website User'"
if filters and filters.get('ignore_user_type'):
user_type_condition = ''
filters.pop('ignore_user_type')

View file

@ -0,0 +1,109 @@
{
"actions": [],
"creation": "2021-01-13 01:51:40.158521",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"column_break_2",
"is_custom",
"permissions_section",
"read",
"write",
"create",
"column_break_8",
"submit",
"cancel",
"amend",
"delete"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "permissions_section",
"fieldtype": "Section Break",
"label": "Role Permissions"
},
{
"default": "1",
"fieldname": "read",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Read"
},
{
"default": "0",
"fieldname": "write",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Write"
},
{
"default": "0",
"fieldname": "create",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Create"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"default": "0",
"fetch_from": "document_type.custom",
"fieldname": "is_custom",
"fieldtype": "Check",
"label": "Is Custom",
"read_only": 1
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "submit",
"fieldtype": "Check",
"label": "Submit"
},
{
"default": "0",
"fieldname": "cancel",
"fieldtype": "Check",
"label": "Cancel"
},
{
"default": "0",
"fieldname": "amend",
"fieldtype": "Check",
"label": "Amend"
},
{
"default": "0",
"fieldname": "delete",
"fieldtype": "Check",
"label": "Delete"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-03-16 00:32:24.414313",
"modified_by": "Administrator",
"module": "Core",
"name": "User Document Type",
"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 UserDocumentType(Document):
pass

View file

@ -0,0 +1,33 @@
{
"actions": [],
"creation": "2021-01-17 18:28:14.208576",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-01-17 18:45:44.993190",
"modified_by": "Administrator",
"module": "Core",
"name": "User Select Document Type",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"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 UserSelectDocumentType(Document):
pass

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 TestUserType(unittest.TestCase):
pass

View file

@ -0,0 +1,77 @@
// Copyright (c) 2021, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('User Type', {
refresh: function(frm) {
frm.toggle_display('is_standard', frappe.boot.developer_mode);
frm.set_df_property('is_standard', 'read_only', !frappe.boot.developer_mode);
const fields = ['role', 'apply_user_permission_on', 'user_id_field',
'user_doctypes', 'user_type_modules'];
frm.toggle_display(fields, !frm.doc.is_standard);
frm.set_query('document_type', 'user_doctypes', function() {
return {
filters: {
istable: 0
}
};
});
frm.set_query('document_type', 'select_doctypes', function() {
return {
filters: {
istable: 0
}
};
});
frm.set_query('document_type', 'custom_select_doctypes', function() {
return {
filters: {
istable: 0
}
};
});
frm.set_query('role', function() {
return {
filters: {
is_custom: 1,
disabled: 0,
desk_access: 1
}
};
});
frm.set_query('apply_user_permission_on', function() {
return {
query: "frappe.core.doctype.user_type.user_type.get_user_linked_doctypes"
};
});
},
onload: function(frm) {
frm.trigger('get_user_id_fields');
},
apply_user_permission_on: function(frm) {
frm.set_value('user_id_field', '');
frm.trigger('get_user_id_fields');
},
get_user_id_fields: function(frm) {
if (frm.doc.apply_user_permission_on) {
frappe.call({
method: 'frappe.core.doctype.user_type.user_type.get_user_id',
args: {
parent: frm.doc.apply_user_permission_on
},
callback: function(r) {
set_field_options('user_id_field', [""].concat(r.message));
}
});
}
}
});

View file

@ -0,0 +1,141 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2021-01-13 01:48:02.378548",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"is_standard",
"section_break_2",
"role",
"column_break_4",
"apply_user_permission_on",
"user_id_field",
"section_break_6",
"user_doctypes",
"custom_select_doctypes",
"select_doctypes",
"allowed_modules_section",
"user_type_modules"
],
"fields": [
{
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard"
},
{
"depends_on": "eval: !doc.is_standard",
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"label": "Document Types and Permissions"
},
{
"fieldname": "user_doctypes",
"fieldtype": "Table",
"label": "Document Types",
"mandatory_depends_on": "eval: !doc.is_standard",
"options": "User Document Type",
"read_only": 1
},
{
"fieldname": "role",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Role",
"mandatory_depends_on": "eval: !doc.is_standard",
"options": "Role",
"read_only": 1
},
{
"fieldname": "select_doctypes",
"fieldtype": "Table",
"hidden": 1,
"label": "Document Types (Select Permissions Only)",
"options": "User Select Document Type",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"description": "Can only list down the document types which has been linked to the User document type.",
"fieldname": "apply_user_permission_on",
"fieldtype": "Link",
"label": "Apply User Permission On",
"mandatory_depends_on": "eval: !doc.is_standard",
"options": "DocType",
"read_only": 1
},
{
"depends_on": "eval: !doc.is_standard",
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"depends_on": "apply_user_permission_on",
"fieldname": "user_id_field",
"fieldtype": "Select",
"label": "User Id Field",
"mandatory_depends_on": "eval: !doc.is_standard",
"read_only": 1
},
{
"depends_on": "eval: !doc.is_standard",
"fieldname": "allowed_modules_section",
"fieldtype": "Section Break",
"label": "Allowed Modules"
},
{
"fieldname": "user_type_modules",
"fieldtype": "Table",
"no_copy": 1,
"options": "User Type Module",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "custom_select_doctypes",
"fieldtype": "Table",
"label": "Custom Document Types (Select Permission)",
"options": "User Select Document Type"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-03-12 16:25:18.639050",
"modified_by": "Administrator",
"module": "Core",
"name": "User Type",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,271 @@
# -*- 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 import _
from six import iteritems
from frappe.utils import get_link_to_form
from frappe.config import get_modules_from_app
from frappe.permissions import add_permission, add_user_permission
from frappe.model.document import Document
class UserType(Document):
def validate(self):
self.set_modules()
self.add_select_perm_doctypes()
def on_update(self):
if self.is_standard:
return
self.validate_document_type_limit()
self.validate_role()
self.add_role_permissions_for_user_doctypes()
self.add_role_permissions_for_select_doctypes()
self.add_role_permissions_for_file()
self.update_users()
get_non_standard_user_type_details()
self.remove_permission_for_deleted_doctypes()
def on_trash(self):
if self.is_standard:
frappe.throw(_('Standard user type {0} can not be deleted.')
.format(frappe.bold(self.name)))
def set_modules(self):
if not self.user_doctypes:
return
modules = frappe.get_all('DocType', fields=['distinct module as module'],
filters={'name': ('in', [d.document_type for d in self.user_doctypes])})
self.set('user_type_modules', [])
for row in modules:
self.append('user_type_modules', {
'module': row.module
})
def validate_document_type_limit(self):
limit = frappe.conf.get('user_type_doctype_limit', {}).get(frappe.scrub(self.name))
if not limit and frappe.session.user != 'Administrator':
frappe.throw(_('User does not have permission to create the new {0}')
.format(frappe.bold(_('User Type'))), title=_('Permission Error'))
if not limit:
frappe.throw(_('The limit has not set for the user type {0} in the site config file.')
.format(frappe.bold(self.name)), title=_('Set Limit'))
if self.user_doctypes and len(self.user_doctypes) > limit:
frappe.throw(_('The total number of user document types limit has been crossed.'),
title=_('User Document Types Limit Exceeded'))
custom_doctypes = [row.document_type for row in self.user_doctypes if row.is_custom]
if custom_doctypes and len(custom_doctypes) > 3:
frappe.throw(_('You can only set the 3 custom doctypes in the Document Types table.'),
title=_('Custom Document Types Limit Exceeded'))
def validate_role(self):
if not self.role:
frappe.throw(_("The field {0} is mandatory")
.format(frappe.bold(_('Role'))))
if not frappe.db.get_value('Role', self.role, 'is_custom'):
frappe.throw(_("The role {0} should be a custom role.")
.format(frappe.bold(get_link_to_form('Role', self.role))))
def update_users(self):
for row in frappe.get_all('User', filters = {'user_type': self.name}):
user = frappe.get_cached_doc('User', row.name)
self.update_roles_in_user(user)
self.update_modules_in_user(user)
user.update_children()
def update_roles_in_user(self, user):
user.set('roles', [])
user.append('roles', {
'role': self.role
})
def update_modules_in_user(self, user):
block_modules = frappe.get_all('Module Def', fields = ['name as module'],
filters={'name': ['not in', [d.module for d in self.user_type_modules]]})
if block_modules:
user.set('block_modules', block_modules)
def add_role_permissions_for_user_doctypes(self):
perms = ['read', 'write', 'create', 'submit', 'cancel', 'amend', 'delete']
for row in self.user_doctypes:
docperm = add_role_permissions(row.document_type, self.role)
values = {perm:row.get(perm) or 0 for perm in perms}
for perm in ['print', 'email', 'share']:
values[perm] = 1
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:
return
self.select_doctypes = []
select_doctypes = []
user_doctypes = tuple([row.document_type for row in self.user_doctypes])
for doctype in user_doctypes:
doc = frappe.get_meta(doctype)
self.prepare_select_perm_doctypes(doc, user_doctypes, select_doctypes)
for child_table in doc.get_table_fields():
if (frappe.db.table_exists(child_table.options)
and not frappe.get_cached_value('DocType', child_table.options, 'istable')):
child_doc = frappe.get_meta(child_table.options)
self.prepare_select_perm_doctypes(child_doc, user_doctypes, select_doctypes)
if select_doctypes:
select_doctypes = set(select_doctypes)
for select_doctype in select_doctypes:
self.append('select_doctypes', {
'document_type': select_doctype
})
def prepare_select_perm_doctypes(self, doc, user_doctypes, select_doctypes):
for field in doc.get_link_fields():
if field.options not in user_doctypes:
select_doctypes.append(field.options)
def add_role_permissions_for_select_doctypes(self):
for doctype in ['select_doctypes', 'custom_select_doctypes']:
for row in self.get(doctype):
docperm = add_role_permissions(row.document_type, self.role)
frappe.db.set_value('Custom DocPerm', docperm,
{'select': 1, 'read': 0, 'create': 0, 'write': 0})
def add_role_permissions_for_file(self):
docperm = add_role_permissions('File', self.role)
frappe.db.set_value('Custom DocPerm', docperm,
{'read': 1, 'create': 1, 'write': 1})
def remove_permission_for_deleted_doctypes(self):
doctypes = [d.document_type for d in self.user_doctypes]
# Do not remove the doc permission for the file doctype
doctypes.append('File')
for doctype in ['select_doctypes', 'custom_select_doctypes']:
for dt in self.get(doctype):
doctypes.append(dt.document_type)
for perm in frappe.get_all('Custom DocPerm',
filters = {'role': self.role, 'parent': ['not in', doctypes]}):
frappe.delete_doc('Custom DocPerm', perm.name)
def add_role_permissions(doctype, role):
name = frappe.get_value('Custom DocPerm', dict(parent=doctype,
role=role, permlevel=0))
if not name:
name = add_permission(doctype, role, 0)
return name
def get_non_standard_user_type_details():
user_types = frappe.get_all('User Type',
fields=['apply_user_permission_on', 'name', 'user_id_field'],
filters={'is_standard': 0})
if user_types:
user_type_details = {d.name: [d.apply_user_permission_on, d.user_id_field] for d in user_types}
frappe.cache().set_value('non_standard_user_types', user_type_details)
return user_type_details
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_user_linked_doctypes(doctype, txt, searchfield, start, page_len, filters):
modules = [d.get('module_name') for d in get_modules_from_app('frappe')]
filters = [['DocField', 'options', '=', 'User'], ['DocType', 'is_submittable', '=', 0],
['DocType', 'issingle', '=', 0], ['DocType', 'module', 'not in', modules],
['DocType', 'read_only', '=', 0], ['DocType', 'name', 'like', '%{0}%'.format(txt)]]
doctypes = frappe.get_all('DocType', fields = ['`tabDocType`.`name`'], filters=filters,
order_by = '`tabDocType`.`idx` desc', limit_start=start, limit_page_length=page_len, as_list=1, debug=1)
custom_dt_filters = [['Custom Field', 'dt', 'like', '%{0}%'.format(txt)],
['Custom Field', 'options', '=', 'User'], ['Custom Field', 'fieldtype', '=', 'Link']]
custom_doctypes = frappe.get_all('Custom Field', fields = ['dt as name'],
filters= custom_dt_filters, as_list=1)
return doctypes + custom_doctypes
@frappe.whitelist()
def get_user_id(parent):
data = frappe.get_all('DocField', fields = ['label', 'fieldname as value'],
filters= {'options': 'User', 'fieldtype': 'Link', 'parent': parent}) or []
data.extend(frappe.get_all('Custom Field', fields = ['label', 'fieldname as value'],
filters= {'options': 'User', 'fieldtype': 'Link', 'dt': parent}))
return data
def user_linked_with_permission_on_doctype(doc, user):
if not doc.apply_user_permission_on:
return True
if not doc.user_id_field:
frappe.throw(_('User Id Field is mandatory in the user type {0}')
.format(frappe.bold(doc.name)))
if frappe.db.get_value(doc.apply_user_permission_on,
{doc.user_id_field: user}, 'name'):
return True
else:
label = frappe.get_meta(doc.apply_user_permission_on).get_field(doc.user_id_field).label
frappe.msgprint(_("To set the role {0} in the user {1}, kindly set the {2} field as {3} in one of the {4} record.")
.format(frappe.bold(doc.role), frappe.bold(user), frappe.bold(label),
frappe.bold(user), frappe.bold(doc.apply_user_permission_on)))
return False
def apply_permissions_for_non_standard_user_type(doc, method=None):
'''Create user permission for the non standard user type'''
if not frappe.db.table_exists('User Type'):
return
user_types = frappe.cache().get_value('non_standard_user_types')
if not user_types:
user_types = get_non_standard_user_type_details()
if not user_types:
return
for user_type, data in iteritems(user_types):
if (not doc.get(data[1]) or doc.doctype != data[0]):
continue
if frappe.get_cached_value('User', doc.get(data[1]), 'user_type') != user_type:
return
if (doc.get(data[1]) and (not doc._doc_before_save or doc.get(data[1]) != doc._doc_before_save.get(data[1])
or not frappe.db.get_value('User Permission',
{'user': doc.get(data[1]), 'allow': data[0], 'for_value': doc.name}, 'name'))):
perm_data = frappe.db.get_value('User Permission',
{'allow': doc.doctype, 'for_value': doc.name}, ['name', 'user'])
if not perm_data:
user_doc = frappe.get_cached_doc('User', doc.get(data[1]))
user_doc.set_roles_and_modules_based_on_user_type()
user_doc.update_children()
add_user_permission(doc.doctype, doc.name, doc.get(data[1]))
else:
frappe.db.set_value('User Permission', perm_data[0], 'user', doc.get(data[1]))

View file

@ -0,0 +1,13 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'user_type',
'transactions': [
{
'label': _('Reference'),
'items': ['User']
}
]
}

View file

@ -0,0 +1,10 @@
frappe.listview_settings['User Type'] = {
add_fields: ["is_standard"],
get_indicator: function (doc) {
if (doc.is_standard) {
return [__("Standard"), "green", "is_standard,=,1"];
} else {
return [__("Custom"), "blue", "is_standard,=,0"];
}
}
};

View file

@ -0,0 +1,33 @@
{
"actions": [],
"creation": "2021-01-24 03:05:24.634719",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"module"
],
"fields": [
{
"fieldname": "module",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Module",
"options": "Module Def",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-01-24 03:07:43.602927",
"modified_by": "Administrator",
"module": "Core",
"name": "User Type Module",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"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 UserTypeModule(Document):
pass

View file

@ -30,8 +30,16 @@ def get_roles_and_doctypes():
"restrict_to_domain": ("in", active_domains)
}, fields=["name"])
restricted_roles = ['Administrator']
if frappe.session.user != 'Administrator':
custom_user_type_roles = frappe.get_all('User Type', filters = {'is_standard': 0}, fields=['role'])
for row in custom_user_type_roles:
restricted_roles.append(row.role)
restricted_roles.append('All')
roles = frappe.get_all("Role", filters={
"name": ("not in", "Administrator"),
"name": ("not in", restricted_roles),
"disabled": 0,
}, or_filters={
"ifnull(restrict_to_domain, '')": "",
@ -54,9 +62,14 @@ def get_permissions(doctype=None, role=None):
if doctype:
out = [p for p in out if p.parent == doctype]
else:
out = frappe.get_all('Custom DocPerm', fields='*', filters=dict(parent = doctype), order_by="permlevel")
filters=dict(parent = doctype)
if frappe.session.user != 'Administrator':
custom_roles = frappe.get_all('Role', filters={'is_custom': 1})
filters['role'] = ['not in', [row.name for row in custom_roles]]
out = frappe.get_all('Custom DocPerm', fields='*', filters=filters, order_by="permlevel")
if not out:
out = frappe.get_all('DocPerm', fields='*', filters=dict(parent = doctype), order_by="permlevel")
out = frappe.get_all('DocPerm', fields='*', filters=filters, order_by="permlevel")
linked_doctypes = {}
for d in out:
@ -78,14 +91,14 @@ def add(parent, role, permlevel):
@frappe.whitelist()
def update(doctype, role, permlevel, ptype, value=None):
"""Update role permission params
Args:
doctype (str): Name of the DocType to update params for
role (str): Role to be updated for, eg "Website Manager".
permlevel (int): perm level the provided rule applies to
ptype (str): permission type, example "read", "delete", etc.
value (None, optional): value for ptype, None indicates False
Returns:
str: Refresh flag is permission is updated successfully
"""

View file

@ -10,6 +10,7 @@
"hide_custom": 0,
"icon": "users",
"idx": 0,
"is_default": 0,
"is_standard": 1,
"label": "Users",
"links": [
@ -135,7 +136,7 @@
"type": "Link"
}
],
"modified": "2020-12-01 13:38:40.085519",
"modified": "2021-03-25 23:02:34.582569",
"modified_by": "Administrator",
"module": "Core",
"name": "Users",
@ -162,6 +163,12 @@
"label": "User Profile",
"link_to": "user-profile",
"type": "Page"
},
{
"doc_view": "",
"label": "User Type",
"link_to": "User Type",
"type": "DocType"
}
]
}

View file

@ -455,6 +455,7 @@ class Database(object):
elif (not ignore) and frappe.db.is_table_missing(e):
# table not found, look in singles
out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update)
else:
raise
else:
@ -507,6 +508,7 @@ class Database(object):
else:
return r and [[i[1] for i in r]] or []
def get_singles_dict(self, doctype, debug = False):
"""Get Single DocType as dict.

View file

@ -30,6 +30,9 @@ class DBTable:
self.get_columns_from_docfields()
def sync(self):
if self.meta.get('is_virtual'):
# no schema to sync for virtual doctypes
return
if self.is_new():
self.create()
else:

View file

@ -5,8 +5,12 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.model.db_query import DatabaseQuery
from frappe.permissions import add_permission, reset_perms
from frappe.core.doctype.doctype.doctype import clear_permissions_cache
# test_records = frappe.get_test_records('ToDo')
test_user_records = frappe.get_test_records('User')
class TestToDo(unittest.TestCase):
def test_delete(self):
@ -47,6 +51,62 @@ class TestToDo(unittest.TestCase):
self.assertEqual(todo.assigned_by_full_name,
frappe.db.get_value('User', todo.assigned_by, 'full_name'))
def test_todo_list_access(self):
create_new_todo('Test1', 'testperm@example.com')
frappe.set_user('test4@example.com')
create_new_todo('Test2', 'test4@example.com')
test_user_data = DatabaseQuery('ToDo').execute()
frappe.set_user('testperm@example.com')
system_manager_data = DatabaseQuery('ToDo').execute()
self.assertNotEqual(test_user_data, system_manager_data)
frappe.set_user('Administrator')
frappe.db.rollback()
def test_doc_read_access(self):
#owner and assigned_by is testperm
todo1 = create_new_todo('Test1', 'testperm@example.com')
test_user = frappe.get_doc('User', 'test4@example.com')
#owner is testperm, but assigned_by is test4
todo2 = create_new_todo('Test2', 'test4@example.com')
frappe.set_user('test4@example.com')
#owner and assigned_by is test4
todo3 = create_new_todo('Test3', 'test4@example.com')
# user without any role to read or write todo document
self.assertFalse(todo1.has_permission("read"))
self.assertFalse(todo1.has_permission("write"))
# user without any role but he/she is assigned_by of that todo document
self.assertTrue(todo2.has_permission("read"))
self.assertTrue(todo2.has_permission("write"))
# user is the owner and assigned_by of the todo document
self.assertTrue(todo3.has_permission("read"))
self.assertTrue(todo3.has_permission("write"))
frappe.set_user('Administrator')
test_user.add_roles('Blogger')
add_permission('ToDo', 'Blogger')
frappe.set_user('test4@example.com')
# user with only read access to todo document, not an owner or assigned_by
self.assertTrue(todo1.has_permission("read"))
self.assertFalse(todo1.has_permission("write"))
frappe.set_user('Administrator')
test_user.remove_roles('Blogger')
reset_perms('ToDo')
clear_permissions_cache('ToDo')
frappe.db.rollback()
def test_fetch_if_empty(self):
frappe.db.sql('delete from tabToDo')
@ -74,3 +134,11 @@ def test_fetch_if_empty(self):
self.assertEqual(todo.assigned_by_full_name,
frappe.db.get_value('User', todo.assigned_by, 'full_name'))
def create_new_todo(description, assigned_by):
todo = {
'doctype': 'ToDo',
'description': description,
'assigned_by': assigned_by
}
return frappe.get_doc(todo).insert()

View file

@ -85,21 +85,30 @@ class ToDo(Document):
else:
raise
# NOTE: todo is viewable if either owner or assigned_to or System Manager in roles
# NOTE: todo is viewable if a user is an owner, or set as assigned_to value, or has any role that is allowed to access ToDo doctype.
def on_doctype_update():
frappe.db.add_index("ToDo", ["reference_type", "reference_name"])
def get_permission_query_conditions(user):
if not user: user = frappe.session.user
if "System Manager" in frappe.get_roles(user):
todo_roles = frappe.permissions.get_doctype_roles('ToDo')
if 'All' in todo_roles:
todo_roles.remove('All')
if any(check in todo_roles for check in frappe.get_roles(user)):
return None
else:
return """(`tabToDo`.owner = {user} or `tabToDo`.assigned_by = {user})"""\
.format(user=frappe.db.escape(user))
def has_permission(doc, user):
if "System Manager" in frappe.get_roles(user):
def has_permission(doc, ptype="read", user=None):
user = user or frappe.session.user
todo_roles = frappe.permissions.get_doctype_roles('ToDo', ptype)
if 'All' in todo_roles:
todo_roles.remove('All')
if any(check in todo_roles for check in frappe.get_roles(user)):
return True
else:
return doc.owner==user or doc.assigned_by==user

View file

@ -248,4 +248,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
}
}

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

@ -13,13 +13,20 @@ from frappe import _
from six import string_types, StringIO
from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cstr, format_duration
from frappe.model.base_document import get_controller
@frappe.whitelist(allow_guest=True)
@frappe.read_only()
def get():
args = get_form_params()
return compress(execute(**args), args=args)
# If virtual doctype get data from controller het_list method
if frappe.db.get_value("DocType", filters={"name": args.doctype}, fieldname="is_virtual"):
controller = get_controller(args.doctype)
data = compress(controller(args.doctype).get_list(args))
else:
data = compress(execute(**args), args=args)
return data
@frappe.whitelist()
@frappe.read_only()
@ -31,7 +38,9 @@ def get_list():
@frappe.read_only()
def get_count():
args = get_form_params()
args.fields = ['{distinct}count(name) as total_count'.format(distinct = 'distinct ' if args.distinct=='true' else '')]
distinct = 'distinct ' if args.distinct=='true' else ''
args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"]
return execute(**args)[0].get('total_count')
def execute(doctype, *args, **kwargs):
@ -429,8 +438,9 @@ def get_stats(stats, doctype, filters=[]):
try:
columns = frappe.db.get_table_columns(doctype)
except frappe.db.InternalError:
except (frappe.db.InternalError, frappe.db.ProgrammingError):
# raised when _user_tags column is added on the fly
# raised if its a virtual doctype
columns = []
for tag in tags:
@ -541,7 +551,7 @@ def get_filters_cond(doctype, filters, conditions, ignore_permissions=None, with
if isinstance(f[1], string_types) and f[1][0] == '!':
flt.append([doctype, f[0], '!=', f[1][1:]])
elif isinstance(f[1], (list, tuple)) and \
f[1][0] in (">", "<", ">=", "<=", "like", "not like", "in", "not in", "between"):
f[1][0] in (">", "<", ">=", "<=", "!=", "like", "not like", "in", "not in", "between"):
flt.append([doctype, f[0], f[1][0], f[1][1]])
else:

View file

@ -36,20 +36,27 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters):
return out
@frappe.whitelist()
def get_children(doctype, parent='', **filters):
def get_children(doctype, parent=''):
return _get_children(doctype, parent)
def _get_children(doctype, parent='', ignore_permissions=False):
parent_field = 'parent_' + doctype.lower().replace(' ', '_')
filters=[['ifnull(`{0}`,"")'.format(parent_field), '=', parent],
filters = [['ifnull(`{0}`,"")'.format(parent_field), '=', parent],
['docstatus', '<' ,'2']]
doctype_meta = frappe.get_meta(doctype)
data = frappe.get_list(doctype, fields=[
'name as value',
'{0} as title'.format(doctype_meta.get('title_field') or 'name'),
'is_group as expandable'],
filters=filters,
order_by='name')
meta = frappe.get_meta(doctype)
return data
return frappe.get_list(
doctype,
fields=[
'name as value',
'{0} as title'.format(meta.get('title_field') or 'name'),
'is_group as expandable'
],
filters=filters,
order_by='name',
ignore_permissions=ignore_permissions
)
@frappe.whitelist()
def add_node():

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

@ -56,12 +56,7 @@ def execute_cmd(cmd, from_async=False):
try:
method = get_attr(cmd)
except Exception as e:
if frappe.local.conf.developer_mode:
raise e
else:
frappe.respond_as_web_page(title='Invalid Method', html='Method not found',
indicator_color='red', http_status_code=404)
return
frappe.throw(_('Invalid Method'))
if from_async:
method = method.queue

View file

@ -147,6 +147,7 @@ doc_events = {
"frappe.core.doctype.file.file.attach_files_to_document",
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers",
"frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date",
"frappe.core.doctype.user_type.user_type.apply_permissions_for_non_standard_user_type"
],
"after_rename": "frappe.desk.notifications.clear_doctype_notifications",
"on_cancel": [

View file

@ -555,22 +555,25 @@ class BaseDocument(object):
not _df.get('fetch_if_empty')
or (_df.get('fetch_if_empty') and not self.get(_df.fieldname))
]
if not frappe.get_meta(doctype).get('is_virtual'):
if not fields_to_fetch:
# cache a single value type
values = frappe._dict(name=frappe.db.get_value(doctype, docname,
'name', cache=True))
else:
values_to_fetch = ['name'] + [_df.fetch_from.split('.')[-1]
for _df in fields_to_fetch]
if not fields_to_fetch:
# cache a single value type
values = frappe._dict(name=frappe.db.get_value(doctype, docname,
'name', cache=True))
else:
values_to_fetch = ['name'] + [_df.fetch_from.split('.')[-1]
for _df in fields_to_fetch]
# don't cache if fetching other values too
values = frappe.db.get_value(doctype, docname,
values_to_fetch, as_dict=True)
# don't cache if fetching other values too
values = frappe.db.get_value(doctype, docname,
values_to_fetch, as_dict=True)
if frappe.get_meta(doctype).issingle:
values.name = doctype
if frappe.get_meta(doctype).get('is_virtual'):
values = frappe.get_doc(doctype, docname)
if values:
setattr(self, df.fieldname, values.name)
@ -792,7 +795,7 @@ class BaseDocument(object):
def _save_passwords(self):
"""Save password field values in __Auth table"""
from frappe.utils.password import set_encrypted_password
from frappe.utils.password import set_encrypted_password, remove_encrypted_password
if self.flags.ignore_save_passwords is True:
return
@ -800,6 +803,10 @@ class BaseDocument(object):
for df in self.meta.get('fields', {'fieldtype': ('=', 'Password')}):
if self.flags.ignore_save_passwords and df.fieldname in self.flags.ignore_save_passwords: continue
new_password = self.get(df.fieldname)
if not new_password:
remove_encrypted_password(self.doctype, self.name, df.fieldname)
if new_password and not self.is_dummy_password(new_password):
# is not a dummy password like '*****'
set_encrypted_password(self.doctype, self.name, new_password, df.fieldname)

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

@ -697,7 +697,7 @@ class Document(BaseDocument):
`self.check_docstatus_transition`."""
conflict = False
self._action = "save"
if not self.get('__islocal'):
if not self.get('__islocal') and not self.meta.get('is_virtual'):
if self.meta.issingle:
modified = frappe.db.sql("""select value from tabSingles
where doctype=%s and field='modified' for update""", self.doctype)

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

@ -247,6 +247,21 @@ def make_boilerplate(template, doc, opts=None):
base_class = 'NestedSet'
base_class_import = 'from frappe.utils.nestedset import NestedSet'
custom_controller = 'pass'
if doc.get('is_virtual'):
custom_controller = """
def db_insert(self):
pass
def load_from_db(self):
pass
def db_update(self):
pass
def get_list(self, args):
pass"""
with open(target_file_path, 'w') as target:
with open(os.path.join(get_module_path("core"), "doctype", scrub(doc.doctype),
"boilerplate", template), 'r') as source:
@ -257,5 +272,6 @@ def make_boilerplate(template, doc, opts=None):
classname=doc.name.replace(" ", ""),
base_class_import=base_class_import,
base_class=base_class,
doctype=doc.name, **opts)
doctype=doc.name, **opts,
custom_controller=custom_controller)
))

View file

@ -280,6 +280,7 @@ frappe.patches.v12_0.remove_example_email_thread_notify
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders()
frappe.patches.v12_0.set_correct_url_in_files
frappe.patches.v13_0.website_theme_custom_scss
frappe.patches.v13_0.make_user_type
frappe.patches.v13_0.set_existing_dashboard_charts_as_public
frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
frappe.patches.v13_0.migrate_translation_column_data

View file

@ -0,0 +1,12 @@
import frappe
from frappe.utils.install import create_user_type
def execute():
frappe.reload_doc('core', 'doctype', 'role')
frappe.reload_doc('core', 'doctype', 'user_document_type')
frappe.reload_doc('core', 'doctype', 'user_type_module')
frappe.reload_doc('core', 'doctype', 'user_select_document_type')
frappe.reload_doc('core', 'doctype', 'user_type')
create_user_type()

View file

@ -7,6 +7,9 @@ import frappe
def execute():
if frappe.db.table_exists('List View Setting'):
if not frappe.db.table_exists('List View Settings'):
frappe.reload_doc("desk", "doctype", "List View Settings")
existing_list_view_settings = frappe.get_all('List View Settings', as_list=True)
for list_view_setting in frappe.get_all('List View Setting', fields = ['disable_count', 'disable_sidebar_stats', 'disable_auto_refresh', 'name']):
name = list_view_setting.pop('name')
@ -16,5 +19,6 @@ def execute():
# setting name here is necessary because autoname is set as prompt
list_view_settings.name = name
list_view_settings.insert()
frappe.delete_doc("DocType", "List View Setting", force=True)
frappe.db.commit()

View file

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

View file

@ -78,14 +78,14 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra
push_perm_check_log(_('User {0} does not have doctype access via role permission for document {1}').format(frappe.bold(user), frappe.bold(doctype)))
def false_if_not_shared():
if ptype in ("read", "write", "share", "email", "print"):
if ptype in ("read", "write", "share", "submit", "email", "print"):
shared = frappe.share.get_shared(doctype, user,
["read" if ptype in ("email", "print") else ptype])
if doc:
doc_name = get_doc_name(doc)
if doc_name in shared:
if ptype in ("read", "write", "share") or meta.permissions[0].get(ptype):
if ptype in ("read", "write", "share", "submit") or meta.permissions[0].get(ptype):
return True
elif shared:
@ -366,6 +366,11 @@ def get_roles(user=None, with_standard=True):
return roles
def get_doctype_roles(doctype, access_type="read"):
"""Returns a list of roles that are allowed to access passed doctype."""
meta = frappe.get_meta(doctype)
return [d.role for d in meta.get("permissions") if d.get(access_type)]
def get_perms_for(roles, perm_doctype='DocPerm'):
'''Get perms for given roles'''
filters = {
@ -475,7 +480,7 @@ def setup_custom_perms(parent):
copy_perms(parent)
return True
def add_permission(doctype, role, permlevel=0):
def add_permission(doctype, role, permlevel=0, ptype=None):
'''Add a new permission rule to the given doctype
for the given Role and Permission Level'''
from frappe.core.doctype.doctype.doctype import validate_permissions_for_doctype
@ -485,6 +490,9 @@ def add_permission(doctype, role, permlevel=0):
permlevel=permlevel, if_owner=0)):
return
if not ptype:
ptype = 'read'
custom_docperm = frappe.get_doc({
"doctype":"Custom DocPerm",
"__islocal": 1,
@ -492,13 +500,14 @@ def add_permission(doctype, role, permlevel=0):
"parenttype": "DocType",
"parentfield": "permissions",
"role": role,
'read': 1,
"permlevel": permlevel,
ptype: 1,
})
custom_docperm.save()
validate_permissions_for_doctype(doctype)
return custom_docperm.name
def copy_perms(parent):
'''Copy all DocPerm in to Custom DocPerm for the given document'''

View file

@ -79,9 +79,9 @@
"public/less/controls.less",
"public/less/chat.less",
"public/css/fonts/inter/inter.css",
"public/scss/desk.scss",
"node_modules/frappe-charts/dist/frappe-charts.min.css",
"node_modules/plyr/dist/plyr.css"
"node_modules/plyr/dist/plyr.css",
"public/scss/desk.scss"
],
"css/frappe-rtl.css": [
"public/css/bootstrap-rtl.css",

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

@ -100,17 +100,11 @@ frappe.db = {
const fields = [];
return frappe.call({
type: 'GET',
method: 'frappe.desk.reportview.get_count',
args: {
doctype,
filters,
fields,
distinct,
}
}).then(r => {
return r.message.values;
return frappe.xcall('frappe.desk.reportview.get_count', {
doctype,
filters,
fields,
distinct,
});
},
get_link_options(doctype, txt = '', filters={}) {

View file

@ -83,11 +83,16 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
var doctype = this.get_options();
var me = this;
if(!doctype) return;
if (!doctype) return;
let df = this.df;
if (this.frm && this.frm.doctype !== this.df.parent) {
// incase of grid use common df set in grid
df = this.frm.get_docfield(this.doc.parentfield, this.df.fieldname);
}
// set values to fill in the new document
if(this.df.get_route_options_for_new_doc) {
frappe.route_options = this.df.get_route_options_for_new_doc(this);
if (df && df.get_route_options_for_new_doc) {
frappe.route_options = df.get_route_options_for_new_doc(this);
} else {
frappe.route_options = {};
}

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

@ -151,19 +151,23 @@ function get_version_comment(version_doc, text) {
let version_comment = "";
let unlinked_content = "";
Array.from($(text)).forEach(element => {
if ($(element).is('a')) {
version_comment += unlinked_content ? frappe.utils.get_form_link('Version', version_doc.name, true, unlinked_content) : "";
unlinked_content = "";
version_comment += element.outerHTML;
} else {
unlinked_content += element.outerHTML || element.textContent;
try {
Array.from($(text)).forEach(element => {
if ($(element).is('a')) {
version_comment += unlinked_content ? frappe.utils.get_form_link('Version', version_doc.name, true, unlinked_content) : "";
unlinked_content = "";
version_comment += element.outerHTML;
} else {
unlinked_content += element.outerHTML || element.textContent;
}
});
if (unlinked_content) {
version_comment += frappe.utils.get_form_link('Version', version_doc.name, true, unlinked_content);
}
});
if (unlinked_content) {
version_comment += frappe.utils.get_form_link('Version', version_doc.name, true, unlinked_content);
return version_comment;
} catch (e) {
// pass
}
return version_comment;
}
return frappe.utils.get_form_link('Version', version_doc.name, true, text);
}

View file

@ -451,7 +451,7 @@ frappe.ui.form.Form = class FrappeForm {
return this.script_manager.trigger("onload_post_render");
}
},
() => this.focus_on_first_input(),
() => this.is_new() && this.focus_on_first_input(),
() => this.run_after_load_hook(),
() => this.dashboard.after_refresh()
]);
@ -1075,7 +1075,7 @@ frappe.ui.form.Form = class FrappeForm {
}
refresh_field(fname) {
if(this.fields_dict[fname] && this.fields_dict[fname].refresh) {
if (this.fields_dict[fname] && this.fields_dict[fname].refresh) {
this.fields_dict[fname].refresh();
this.layout.refresh_dependency();
}
@ -1241,20 +1241,22 @@ frappe.ui.form.Form = class FrappeForm {
}
}
set_df_property(fieldname, property, value, docname, table_field) {
var df;
set_df_property(fieldname, property, value, docname, table_field, table_row_name=null) {
let df;
if (!docname || !table_field) {
df = this.get_docfield(fieldname);
} else {
var grid = this.fields_dict[fieldname].grid,
fname = frappe.utils.filter_dict(grid.docfields, {'fieldname': table_field});
if (fname && fname.length)
df = frappe.meta.get_docfield(fname[0].parent, table_field, docname);
const grid = this.fields_dict[fieldname].grid;
const filtered_fields = frappe.utils.filter_dict(grid.docfields, {'fieldname': table_field});
if (filtered_fields.length) {
df = frappe.meta.get_docfield(filtered_fields[0].parent, table_field, table_row_name);
}
}
if (df && df[property] != value) {
df[property] = value;
if (!docname || !table_field) {
// do not refresh childtable fields since `this.fields_dict` doesn't have child table fields
if (table_field && table_row_name) {
this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name].refresh_field(fieldname);
} else {
this.refresh_field(fieldname);
}
}

View file

@ -6,11 +6,10 @@ frappe.ui.form.FormViewers = class FormViewers {
}
refresh() {
// REDESIGN-TODO: fix this
// let users = this.frm.get_docinfo()['viewers'];
// let currently_viewing = users.current.filter(user => user != frappe.session.user);
// let avatar_group = frappe.avatar_group(currently_viewing, 5, {'align': 'left', 'overlap': true});
this.parent.empty(); //.append(avatar_group);
let users = this.frm.get_docinfo()['viewers'];
let currently_viewing = users.current.filter(user => user != frappe.session.user);
let avatar_group = frappe.avatar_group(currently_viewing, 5, {'align': 'left', 'overlap': true});
this.parent.empty().append(avatar_group);
}
};

View file

@ -4,9 +4,12 @@ export default class GridRow {
constructor(opts) {
this.on_grid_fields_dict = {};
this.on_grid_fields = [];
$.extend(this, opts);
if (this.doc && this.parent_df.options) {
this.docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
}
this.columns = {};
this.columns_list = [];
$.extend(this, opts);
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">';
this.make();
}
@ -153,7 +156,7 @@ export default class GridRow {
this.render_row(true);
}
// refersh form fields
// refresh form fields
if(this.grid_form) {
this.grid_form.layout && this.grid_form.layout.refresh(this.doc);
}
@ -249,27 +252,29 @@ export default class GridRow {
this.focus_set = false;
this.grid.setup_visible_columns();
for(var ci in this.grid.visible_columns) {
var df = this.grid.visible_columns[ci][0],
colsize = this.grid.visible_columns[ci][1],
txt = this.doc ?
frappe.format(this.doc[df.fieldname], df, null, this.doc) :
__(df.label);
this.grid.visible_columns.forEach((col, ci) => {
// to get update df for the row
let df = this.docfields.find(field => field.fieldname === col[0].fieldname);
if(this.doc && df.fieldtype === "Select") {
let colsize = col[1];
let txt = this.doc ?
frappe.format(this.doc[df.fieldname], df, null, this.doc) :
__(df.label);
if (this.doc && df.fieldtype === "Select") {
txt = __(txt);
}
if(!this.columns[df.fieldname]) {
var column = this.make_column(df, colsize, txt, ci);
let column;
if (!this.columns[df.fieldname]) {
column = this.make_column(df, colsize, txt, ci);
} else {
var column = this.columns[df.fieldname];
column = this.columns[df.fieldname];
this.refresh_field(df.fieldname, txt);
}
// background color for cellz
if(this.doc) {
if(df.reqd && !txt) {
// background color for cell
if (this.doc) {
if (df.reqd && !txt) {
column.addClass('error');
}
if (column.is_invalid) {
@ -278,7 +283,7 @@ export default class GridRow {
column.addClass('bold');
}
}
}
});
}
make_column(df, colsize, txt, ci) {
@ -403,9 +408,9 @@ export default class GridRow {
if (!field.df.onchange_modified) {
var field_on_change_function = field.df.onchange;
field.df.onchange = function(e) {
field.df.onchange = (e) => {
field_on_change_function && field_on_change_function(e);
me.grid.grid_rows[this.doc.idx - 1].refresh_field(this.df.fieldname);
this.refresh_field(field.df.fieldname);
};
field.df.onchange_modified = true;
@ -589,42 +594,37 @@ export default class GridRow {
}
}
refresh_field(fieldname, txt) {
var df = this.grid.get_docfield(fieldname) || undefined;
let df = this.docfields.find(col => {
return col.fieldname === fieldname;
});
// format values if no frm
if(!df) {
df = this.grid.visible_columns.find((col) => {
return col[0].fieldname === fieldname;
});
if(df && this.doc) {
var txt = frappe.format(this.doc[fieldname], df[0],
null, this.doc);
}
if (df && this.doc) {
txt = frappe.format(this.doc[fieldname], df, null, this.doc);
}
if(txt===undefined && this.frm) {
var txt = frappe.format(this.doc[fieldname], df,
null, this.frm.doc);
if (!txt && this.frm) {
txt = frappe.format(this.doc[fieldname], df, null, this.frm.doc);
}
// reset static value
var column = this.columns[fieldname];
if(column) {
let column = this.columns[fieldname];
if (column) {
column.static_area.html(txt || "");
if(df && df.reqd) {
column.toggleClass('error', !!(txt===null || txt===''));
if (df && df.reqd) {
column.toggleClass('error', !!(txt === null || txt === ''));
}
}
let field = this.on_grid_fields_dict[fieldname];
// reset field value
var field = this.on_grid_fields_dict[fieldname];
if(field) {
if (field) {
field.docname = this.doc.name;
field.refresh();
}
// in form
if(this.grid_form) {
if (this.grid_form) {
this.grid_form.refresh_field(fieldname);
}
}

View file

@ -319,7 +319,7 @@ frappe.ui.form.Layout = Class.extend({
fieldobj.doctype = me.doc.doctype;
fieldobj.docname = me.doc.name;
fieldobj.df = frappe.meta.get_docfield(me.doc.doctype,
fieldobj.df.fieldname, me.frm ? me.frm.doc.name : me.doc.name) || fieldobj.df;
fieldobj.df.fieldname, me.doc.name) || fieldobj.df;
// on form change, permissions can change
if (me.frm) {
@ -512,7 +512,7 @@ frappe.ui.form.Layout = Class.extend({
if (form_obj) {
if (this.doc && this.doc.parent) {
form_obj.setting_dependency = true;
form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname);
form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname, this.doc.name);
form_obj.setting_dependency = false;
// refresh child fields
this.fields_dict[fieldname] && this.fields_dict[fieldname].refresh();

View file

@ -112,7 +112,6 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
};
var check_mandatory = function () {
var me = this;
var has_errors = false;
frm.scroll_set = false;
@ -124,8 +123,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
$.each(frappe.meta.docfield_list[doc.doctype] || [], function (i, docfield) {
if (docfield.fieldname) {
var df = frappe.meta.get_docfield(doc.doctype,
docfield.fieldname, frm.doc.name);
const df = frappe.meta.get_docfield(doc.doctype,
docfield.fieldname, doc.name);
if (df.fieldtype === "Fold") {
folded = frm.layout.folded;

View file

@ -123,6 +123,7 @@ frappe.ui.form.Share = Class.extend({
user: user,
read: $(d.body).find(".add-share-read").prop("checked") ? 1 : 0,
write: $(d.body).find(".add-share-write").prop("checked") ? 1 : 0,
submit: $(d.body).find(".add-share-submit").prop("checked") ? 1 : 0,
share: $(d.body).find(".add-share-share").prop("checked") ? 1 : 0,
notify: 1,
},

View file

@ -1,13 +1,14 @@
<div>
<div class="row">
<div class="col-xs-6"><h6>{%= __("User") %}</h6></div>
<div class="col-xs-4"><h6>{%= __("User") %}</h6></div>
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Read") %}</h6></div>
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Write") %}</h6></div>
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Submit") %}</h6></div>
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Share") %}</h6></div>
</div>
<div class="row shared-user" data-everyone=1>
<div class="col-xs-6 share-all"><b>{{ __("Everyone") }}</b></div>
<div class="col-xs-4 share-all"><b>{{ __("Everyone") }}</b></div>
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="read"
{% if(cint(everyone.read)) { %}checked{% } %} class="edit-share"></div>
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="write"
@ -15,6 +16,11 @@
{% if(cint(everyone.write)) { %}checked{% } %}
{% if (!frm.perm[0].write){ %} disabled="disabled"{% } %}>
</div>
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="submit"
class="edit-share"
{% if(cint(everyone.submit)) { %}checked{% } %}
{% if (!frm.perm[0].submit){ %} disabled="disabled"{% } %}>
</div>
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="share"
{% if(cint(everyone.share)) { %}checked{% } %} class="edit-share"></div>
</div>
@ -23,11 +29,13 @@
var s = shared[i]; %}
{% if(s && !s.everyone) { %}
<div class="row shared-user" data-user="{%= s.user %}" data-name="{%= s.name %}">
<div class="col-xs-6">{%= s.user %}</div>
<div class="col-xs-4">{%= s.user %}</div>
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="read"
{% if(cint(s.read)) { %}checked{% } %} class="edit-share"></div>
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="write"
{% if(cint(s.write)) { %}checked{% } %} class="edit-share"{% if (!frm.perm[0].write){ %} disabled="disabled"{% } %}></div>
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="submit"
{% if(cint(s.submit)) { %}checked{% } %} class="edit-share"{% if (!frm.perm[0].submit){ %} disabled="disabled"{% } %}></div>
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="share"
{% if(cint(s.share)) { %}checked{% } %} class="edit-share"></div>
</div>
@ -38,22 +46,26 @@
<hr>
<div class="row">
<div class="col-xs-6"><h6>{%= __("Share this document with") %}</h6></div>
<div class="col-xs-4"><h6>{%= __("Share this document with") %}</h6></div>
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Read") %}</h6></div>
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Write") %}</h6></div>
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Submit") %}</h6></div>
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Share") %}</h6></div>
</div>
<div class="row">
<div class="col-xs-6 input-wrapper-add-share"></div>
<div class="col-xs-4 input-wrapper-add-share"></div>
<div class="col-xs-2 flex justify-center align-flex-start mt-2"><input type="checkbox" class="add-share-read" name="read"></div>
<div class="col-xs-2 flex justify-center align-flex-start mt-2"><input type="checkbox" class="add-share-write" name="write"
{% if (!frm.perm[0].write){ %} disabled="disabled"{% } %}>
</div>
<div class="col-xs-2 flex justify-center align-flex-start mt-2"><input type="checkbox" class="add-share-submit" name="submit"
{% if (!frm.perm[0].submit){ %} disabled="disabled"{% } %}>
</div>
<div class="col-xs-2 flex justify-center align-flex-start mt-2"><input type="checkbox" class="add-share-share" name="share"></div>
</div>
<div>
<button class="btn btn-primary btn-sm btn-add-share">{{ __("Add") }}</button>
<button class="btn btn-primary btn-sm btn-add-share mt-3">{{ __("Add") }}</button>
</div>
{% endif %}
</div>

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

@ -210,7 +210,10 @@ frappe.ui.form.Toolbar = class Toolbar {
}
make_viewers() {
if (this.frm.viewers) return;
if (this.frm.viewers) {
this.frm.viewers.parent.empty();
return;
}
this.frm.viewers = new frappe.ui.form.FormViewers({
frm: this.frm,
parent: $('<div class="form-viewers d-flex"></div>').prependTo(this.frm.page.page_actions)

View file

@ -256,11 +256,15 @@ $.extend(frappe.model, {
},
can_select: function(doctype) {
return frappe.boot.user.can_select.indexOf(doctype)!==-1;
if (frappe.boot.user) {
return frappe.boot.user.can_select.indexOf(doctype)!==-1;
}
},
can_read: function(doctype) {
return frappe.boot.user.can_read.indexOf(doctype)!==-1;
if (frappe.boot.user) {
return frappe.boot.user.can_read.indexOf(doctype)!==-1;
}
},
can_write: function(doctype) {

View file

@ -85,6 +85,7 @@ $.extend(frappe.perm, {
if (s.user === user) {
perm[0]["read"] = perm[0]["read"] || s.read;
perm[0]["write"] = perm[0]["write"] || s.write;
perm[0]["submit"] = perm[0]["submit"] || s.submit;
perm[0]["share"] = perm[0]["share"] || s.share;
if (s.read) {

View file

@ -159,9 +159,12 @@ frappe.socketio = {
},
doc_open: function(doctype, docname) {
// notify that the user has opened this doc, if not already notified
if(!frappe.socketio.last_doc
|| (frappe.socketio.last_doc[0]!=doctype && frappe.socketio.last_doc[1]!=docname)) {
if (!frappe.socketio.last_doc
|| (frappe.socketio.last_doc[0] != doctype || frappe.socketio.last_doc[1] != docname)) {
frappe.socketio.socket.emit('doc_open', doctype, docname);
frappe.socketio.last_doc &&
frappe.socketio.doc_close(frappe.socketio.last_doc[0], frappe.socketio.last_doc[1]);
}
frappe.socketio.last_doc = [doctype, docname];
},

View file

@ -834,7 +834,7 @@ Object.assign(frappe.utils, {
get_form_link: function(doctype, name, html = false, display_text = null) {
display_text = display_text || name;
name = encodeURIComponent(name);
const route = `/app/${encodeURIComponent(frappe.router.slug(doctype))}/${name}`;
const route = `/app/${encodeURIComponent(doctype.toLowerCase().replace(/ /g, '-'))}/${name}`;
if (html) {
return `<a href="${route}">${display_text}</a>`;
}
@ -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

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

@ -11,8 +11,8 @@
--gray-900: #161a1f;
// Type Colors
--text-muted: var(--gray-300);
--text-light: var(--gray-400);
--text-muted: var(--gray-400);
--text-light: var(--gray-300);
--text-color: var(--gray-50);
--heading-color: var(--gray-50);
@ -114,19 +114,21 @@
// --criticism-bg: var(--red-600);
// Frappe Charts Colors
--charts-label-color: var(--gray-300);
--charts-axis-line-color: var(--gray-500);
.chart-container {
--charts-label-color: var(--gray-300);
--charts-axis-line-color: var(--gray-500);
--charts-stroke-width: 5px;
--charts-dataset-circle-stroke: #ffffff;
--charts-dataset-circle-stroke-width: var(--charts-stroke-width);
--charts-stroke-width: 5px;
--charts-dataset-circle-stroke: #ffffff;
--charts-dataset-circle-stroke-width: var(--charts-stroke-width);
--charts-tooltip-title: var(--charts-label-color);
--charts-tooltip-label: var(--charts-label-color);
--charts-tooltip-value: white;
--charts-tooltip-bg: var(--gray-900);
--charts-tooltip-title: var(--charts-label-color);
--charts-tooltip-label: var(--charts-label-color);
--charts-tooltip-value: white;
--charts-tooltip-bg: var(--gray-900);
--charts-legend-label: var(--charts-label-color);
--charts-legend-label: var(--charts-label-color);
}
// find better fix
.heatmap-chart {

View file

@ -23,7 +23,7 @@
@import "notification";
@import "global_search";
@import "desktop";
@import "awesomebar";
@import "../common/awesomeplete";
@import "sidebar";
@import "filters";
@import "list";

View file

@ -10,6 +10,7 @@
@import "../common/modal";
@import "../common/indicator";
@import "../common/controls";
@import "../common/awesomeplete";
@import 'multilevel_dropdown';
@import 'website_image';
@import 'website_avatar';

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

@ -10,7 +10,7 @@ from frappe.desk.doctype.notification_log.notification_log import enqueue_create
from frappe.utils import cint
@frappe.whitelist()
def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=None, notify=0):
def add(doctype, name, user=None, read=1, write=0, submit=0, share=0, everyone=0, flags=None, notify=0):
"""Share the given document with a user."""
if not user:
user = frappe.session.user
@ -38,6 +38,7 @@ def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=No
# always add read, since you are adding!
"read": 1,
"write": cint(write),
"submit": cint(submit),
"share": cint(share)
})
@ -78,11 +79,11 @@ def set_permission(doctype, name, user, permission_to, value=1, everyone=0):
if not value:
# un-set higher-order permissions too
if permission_to=="read":
share.read = share.write = share.share = 0
share.read = share.write = share.submit = share.share = 0
share.save()
if not (share.read or share.write or share.share):
if not (share.read or share.write or share.submit or share.share):
share.delete()
share = {}
@ -92,7 +93,7 @@ def set_permission(doctype, name, user, permission_to, value=1, everyone=0):
def get_users(doctype, name):
"""Get list of users with which this document is shared"""
return frappe.db.get_all("DocShare",
fields=["`name`", "`user`", "`read`", "`write`", "`share`", "everyone", "owner", "creation"],
fields=["`name`", "`user`", "`read`", "`write`", "`submit`", "`share`", "everyone", "owner", "creation"],
filters=dict(
share_doctype=doctype,
share_name=name

View file

@ -110,5 +110,39 @@
{%- endblock %}
<!-- csrf_token -->
{%- block body_include %}{{ body_include or "" }}{% endblock -%}
<script>
frappe.ready(() => {
if (frappe.session.user === 'Guest') {
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 = [];
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((e) => {
let lang = $("#language-switcher").val();
frappe.call("frappe.translate.set_preferred_language_cookie", {
"preferred_language": lang
}).then(() => {
window.location.reload();
});
});
});
}
})
</script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show more