@@ -12,7 +12,7 @@ jobs: | |||||
- name: 'Setup Environment' | - name: 'Setup Environment' | ||||
uses: actions/setup-python@v2 | uses: actions/setup-python@v2 | ||||
with: | with: | ||||
python-version: 3.6 | |||||
python-version: 3.7 | |||||
- name: 'Clone repo' | - name: 'Clone repo' | ||||
uses: actions/checkout@v2 | uses: actions/checkout@v2 | ||||
@@ -18,7 +18,7 @@ jobs: | |||||
node-version: 14 | node-version: 14 | ||||
- uses: actions/setup-python@v2 | - uses: actions/setup-python@v2 | ||||
with: | with: | ||||
python-version: '3.6' | |||||
python-version: '3.7' | |||||
- name: Set up bench and build assets | - name: Set up bench and build assets | ||||
run: | | run: | | ||||
npm install -g yarn | npm install -g yarn | ||||
@@ -21,7 +21,7 @@ jobs: | |||||
python-version: '12.x' | python-version: '12.x' | ||||
- uses: actions/setup-python@v2 | - uses: actions/setup-python@v2 | ||||
with: | with: | ||||
python-version: '3.6' | |||||
python-version: '3.7' | |||||
- name: Set up bench and build assets | - name: Set up bench and build assets | ||||
run: | | run: | | ||||
npm install -g yarn | npm install -g yarn | ||||
@@ -6,6 +6,28 @@ context('List View', () => { | |||||
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow"); | return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow"); | ||||
}); | }); | ||||
}); | }); | ||||
it('Keep checkbox checked after Bulk Update', () => { | |||||
cy.go_to_list('ToDo'); | |||||
cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true }); | |||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); | |||||
cy.get('.dropdown-menu li:visible .dropdown-item .menu-item-label[data-label="Edit"]').click(); | |||||
cy.get('.modal-body .form-control[data-fieldname="field"]').first().select('Due Date').wait(200); | |||||
cy.get('.modal-body .frappe-control[data-fieldname="value"] input:visible').first().focus(); | |||||
cy.get('.datepickers-container .datepicker.active').should('be.visible'); | |||||
cy.get('.datepickers-container .datepicker.active .datepicker--cell-day.-current-').click({force: true}); | |||||
cy.get('.modal-body .frappe-control[data-fieldname="value"] input:visible').first().focus(); | |||||
cy.get('.datepickers-container .datepicker.active .datepicker--cell-day.-current-').click({force: true}); | |||||
cy.get('.modal-footer .standard-actions .btn-primary').click(); | |||||
cy.wait(500); | |||||
cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); | |||||
cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible'); | |||||
}); | |||||
it('enables "Actions" button', () => { | it('enables "Actions" button', () => { | ||||
const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; | const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; | ||||
cy.go_to_list('ToDo'); | cy.go_to_list('ToDo'); | ||||
@@ -30,4 +52,3 @@ context('List View', () => { | |||||
}); | }); | ||||
}); | }); | ||||
}); | }); | ||||
@@ -235,12 +235,13 @@ def connect_replica(): | |||||
from frappe.database import get_db | from frappe.database import get_db | ||||
user = local.conf.db_name | user = local.conf.db_name | ||||
password = local.conf.db_password | password = local.conf.db_password | ||||
port = local.conf.replica_db_port | |||||
if local.conf.different_credentials_for_replica: | if local.conf.different_credentials_for_replica: | ||||
user = local.conf.replica_db_name | user = local.conf.replica_db_name | ||||
password = local.conf.replica_db_password | password = local.conf.replica_db_password | ||||
local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password) | |||||
local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password, port=port) | |||||
# swap db connections | # swap db connections | ||||
local.primary_db = local.db | local.primary_db = local.db | ||||
@@ -1,6 +1,7 @@ | |||||
# Copyright (c) 2019, Frappe Technologies and contributors | |||||
# Copyright (c) 2021, Frappe Technologies and contributors | |||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import frappe | import frappe | ||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
@@ -10,25 +11,40 @@ class AccessLog(Document): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@frappe.write_only() | @frappe.write_only() | ||||
def make_access_log(doctype=None, document=None, method=None, file_type=None, | |||||
report_name=None, filters=None, page=None, columns=None): | |||||
@retry( | |||||
stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError) | |||||
) | |||||
def make_access_log( | |||||
doctype=None, | |||||
document=None, | |||||
method=None, | |||||
file_type=None, | |||||
report_name=None, | |||||
filters=None, | |||||
page=None, | |||||
columns=None, | |||||
): | |||||
user = frappe.session.user | user = frappe.session.user | ||||
in_request = frappe.request and frappe.request.method == "GET" | |||||
doc = frappe.get_doc({ | |||||
'doctype': 'Access Log', | |||||
'user': user, | |||||
'export_from': doctype, | |||||
'reference_document': document, | |||||
'file_type': file_type, | |||||
'report_name': report_name, | |||||
'page': page, | |||||
'method': method, | |||||
'filters': frappe.utils.cstr(filters) if filters else None, | |||||
'columns': columns | |||||
}) | |||||
doc = frappe.get_doc( | |||||
{ | |||||
"doctype": "Access Log", | |||||
"user": user, | |||||
"export_from": doctype, | |||||
"reference_document": document, | |||||
"file_type": file_type, | |||||
"report_name": report_name, | |||||
"page": page, | |||||
"method": method, | |||||
"filters": frappe.utils.cstr(filters) if filters else None, | |||||
"columns": columns, | |||||
} | |||||
) | |||||
doc.insert(ignore_permissions=True) | doc.insert(ignore_permissions=True) | ||||
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` | # `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` | ||||
if frappe.request and frappe.request.method == 'GET': | |||||
# dont commit in test mode | |||||
if not frappe.flags.in_test or in_request: | |||||
frappe.db.commit() | frappe.db.commit() |
@@ -332,7 +332,7 @@ class Database(object): | |||||
values[key] = value | values[key] = value | ||||
if isinstance(value, (list, tuple)): | if isinstance(value, (list, tuple)): | ||||
# value is a tuple like ("!=", 0) | # value is a tuple like ("!=", 0) | ||||
_operator = value[0] | |||||
_operator = value[0].lower() | |||||
values[key] = value[1] | values[key] = value[1] | ||||
if isinstance(value[1], (tuple, list)): | if isinstance(value[1], (tuple, list)): | ||||
# value is a list in tuple ("in", ("A", "B")) | # value is a list in tuple ("in", ("A", "B")) | ||||
@@ -1,322 +1,106 @@ | |||||
{ | { | ||||
"allow_copy": 0, | |||||
"allow_guest_to_view": 0, | |||||
"allow_import": 0, | |||||
"allow_rename": 1, | |||||
"beta": 0, | |||||
"creation": "2013-05-24 13:41:00", | |||||
"custom": 0, | |||||
"description": "", | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"document_type": "Document", | |||||
"editable_grid": 0, | |||||
"fields": [ | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "title", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 1, | |||||
"in_list_view": 1, | |||||
"in_standard_filter": 0, | |||||
"label": "Title", | |||||
"length": 0, | |||||
"no_copy": 1, | |||||
"permlevel": 0, | |||||
"print_hide": 1, | |||||
"print_hide_if_no_value": 0, | |||||
"read_only": 0, | |||||
"remember_last_selected_value": 0, | |||||
"report_hide": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 1, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"description": "", | |||||
"fieldname": "public", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Public", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"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, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 1, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"depends_on": "public", | |||||
"fieldname": "notify_on_login", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Notify users with a popup when they log in", | |||||
"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, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 1, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "0", | |||||
"depends_on": "notify_on_login", | |||||
"description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.", | |||||
"fieldname": "notify_on_every_login", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Notify Users On Every Login", | |||||
"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, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"depends_on": "eval:doc.notify_on_login && doc.public", | |||||
"fieldname": "expire_notification_on", | |||||
"fieldtype": "Date", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Expire Notification On", | |||||
"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": 1, | |||||
"set_only_once": 0, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 1, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"description": "Help: To link to another record in the system, use \"#Form/Note/[Note Name]\" as the Link URL. (don't use \"http://\")", | |||||
"fieldname": "content", | |||||
"fieldtype": "Text Editor", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 1, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Content", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"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, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 1, | |||||
"columns": 0, | |||||
"fieldname": "seen_by_section", | |||||
"fieldtype": "Section Break", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Seen By", | |||||
"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, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"fieldname": "seen_by", | |||||
"fieldtype": "Table", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"ignore_xss_filter": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"label": "Seen By Table", | |||||
"length": 0, | |||||
"no_copy": 0, | |||||
"options": "Note Seen By", | |||||
"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, | |||||
"translatable": 0, | |||||
"unique": 0 | |||||
} | |||||
], | |||||
"has_web_view": 0, | |||||
"hide_heading": 0, | |||||
"hide_toolbar": 0, | |||||
"icon": "fa fa-file-text", | |||||
"idx": 1, | |||||
"image_view": 0, | |||||
"in_create": 0, | |||||
"is_submittable": 0, | |||||
"issingle": 0, | |||||
"istable": 0, | |||||
"max_attachments": 0, | |||||
"modified": "2018-09-21 15:15:44.909636", | |||||
"modified_by": "Administrator", | |||||
"module": "Desk", | |||||
"name": "Note", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"amend": 0, | |||||
"cancel": 0, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"export": 0, | |||||
"if_owner": 0, | |||||
"import": 0, | |||||
"permlevel": 0, | |||||
"print": 1, | |||||
"read": 1, | |||||
"report": 0, | |||||
"role": "All", | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"submit": 0, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 1, | |||||
"read_only": 0, | |||||
"read_only_onload": 1, | |||||
"show_name_in_global_search": 0, | |||||
"sort_order": "ASC", | |||||
"track_changes": 1, | |||||
"track_seen": 0, | |||||
"track_views": 0 | |||||
} | |||||
"actions": [], | |||||
"allow_rename": 1, | |||||
"creation": "2013-05-24 13:41:00", | |||||
"doctype": "DocType", | |||||
"document_type": "Document", | |||||
"engine": "InnoDB", | |||||
"field_order": [ | |||||
"title", | |||||
"public", | |||||
"notify_on_login", | |||||
"notify_on_every_login", | |||||
"expire_notification_on", | |||||
"content", | |||||
"seen_by_section", | |||||
"seen_by" | |||||
], | |||||
"fields": [ | |||||
{ | |||||
"fieldname": "title", | |||||
"fieldtype": "Data", | |||||
"in_global_search": 1, | |||||
"in_list_view": 1, | |||||
"label": "Title", | |||||
"no_copy": 1, | |||||
"print_hide": 1, | |||||
"reqd": 1 | |||||
}, | |||||
{ | |||||
"bold": 1, | |||||
"default": "0", | |||||
"fieldname": "public", | |||||
"fieldtype": "Check", | |||||
"label": "Public", | |||||
"print_hide": 1 | |||||
}, | |||||
{ | |||||
"bold": 1, | |||||
"default": "0", | |||||
"depends_on": "public", | |||||
"fieldname": "notify_on_login", | |||||
"fieldtype": "Check", | |||||
"label": "Notify users with a popup when they log in" | |||||
}, | |||||
{ | |||||
"bold": 1, | |||||
"default": "0", | |||||
"depends_on": "notify_on_login", | |||||
"description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.", | |||||
"fieldname": "notify_on_every_login", | |||||
"fieldtype": "Check", | |||||
"label": "Notify Users On Every Login" | |||||
}, | |||||
{ | |||||
"depends_on": "eval:doc.notify_on_login && doc.public", | |||||
"fieldname": "expire_notification_on", | |||||
"fieldtype": "Date", | |||||
"label": "Expire Notification On", | |||||
"search_index": 1 | |||||
}, | |||||
{ | |||||
"bold": 1, | |||||
"description": "Help: To link to another record in the system, use \"/app/note/[Note Name]\" as the Link URL. (don't use \"http://\")", | |||||
"fieldname": "content", | |||||
"fieldtype": "Text Editor", | |||||
"in_global_search": 1, | |||||
"label": "Content" | |||||
}, | |||||
{ | |||||
"collapsible": 1, | |||||
"fieldname": "seen_by_section", | |||||
"fieldtype": "Section Break", | |||||
"label": "Seen By" | |||||
}, | |||||
{ | |||||
"fieldname": "seen_by", | |||||
"fieldtype": "Table", | |||||
"label": "Seen By Table", | |||||
"options": "Note Seen By" | |||||
} | |||||
], | |||||
"icon": "fa fa-file-text", | |||||
"idx": 1, | |||||
"links": [], | |||||
"modified": "2021-09-18 10:57:51.352643", | |||||
"modified_by": "Administrator", | |||||
"module": "Desk", | |||||
"name": "Note", | |||||
"owner": "Administrator", | |||||
"permissions": [ | |||||
{ | |||||
"create": 1, | |||||
"delete": 1, | |||||
"email": 1, | |||||
"print": 1, | |||||
"read": 1, | |||||
"role": "All", | |||||
"share": 1, | |||||
"write": 1 | |||||
} | |||||
], | |||||
"quick_entry": 1, | |||||
"sort_field": "modified", | |||||
"sort_order": "ASC", | |||||
"track_changes": 1 | |||||
} |
@@ -4,6 +4,7 @@ | |||||
from typing import List | from typing import List | ||||
import frappe.defaults | import frappe.defaults | ||||
from frappe.query_builder.utils import Column | |||||
import frappe.share | import frappe.share | ||||
from frappe import _ | from frappe import _ | ||||
import frappe.permissions | import frappe.permissions | ||||
@@ -491,7 +492,7 @@ class DatabaseQuery(object): | |||||
f.value = date_range | f.value = date_range | ||||
fallback = "'0001-01-01 00:00:00'" | fallback = "'0001-01-01 00:00:00'" | ||||
if f.operator in ('>', '<') and (f.fieldname in ('creation', 'modified')): | |||||
if (f.fieldname in ('creation', 'modified')): | |||||
value = cstr(f.value) | value = cstr(f.value) | ||||
fallback = "NULL" | fallback = "NULL" | ||||
@@ -547,8 +548,12 @@ class DatabaseQuery(object): | |||||
value = flt(f.value) | value = flt(f.value) | ||||
fallback = 0 | fallback = 0 | ||||
if isinstance(f.value, Column): | |||||
quote = '"' if frappe.conf.db_type == 'postgres' else "`" | |||||
value = f"{tname}.{quote}{f.value.name}{quote}" | |||||
# escape value | # escape value | ||||
if isinstance(value, str) and not f.operator.lower() == 'between': | |||||
elif isinstance(value, str) and not f.operator.lower() == 'between': | |||||
value = f"{frappe.db.escape(value, percent=False)}" | value = f"{frappe.db.escape(value, percent=False)}" | ||||
if ( | if ( | ||||
@@ -212,13 +212,12 @@ export default class Grid { | |||||
delete_all_rows() { | delete_all_rows() { | ||||
frappe.confirm(__("Are you sure you want to delete all rows?"), () => { | frappe.confirm(__("Are you sure you want to delete all rows?"), () => { | ||||
this.grid_rows.forEach(row => { | |||||
row.remove(); | |||||
}); | |||||
this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype); | |||||
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').prop('checked', 0); | |||||
this.frm.doc[this.df.fieldname] = []; | |||||
$(this.parent).find('.rows').empty(); | |||||
this.grid_rows = []; | |||||
this.refresh(); | this.refresh(); | ||||
this.frm && this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype); | |||||
this.frm && this.frm.dirty(); | |||||
this.scroll_to_top(); | this.scroll_to_top(); | ||||
}); | }); | ||||
} | } | ||||
@@ -244,8 +243,10 @@ export default class Grid { | |||||
this.remove_rows_button.toggleClass('hidden', | this.remove_rows_button.toggleClass('hidden', | ||||
this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); | this.wrapper.find('.grid-body .grid-row-check:checked:first').length ? false : true); | ||||
this.remove_all_rows_button.toggleClass('hidden', | |||||
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').length ? false : true); | |||||
let select_all_checkbox_checked = this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').length; | |||||
let show_delete_all_btn = select_all_checkbox_checked && this.data.length > this.get_selected_children().length; | |||||
this.remove_all_rows_button.toggleClass('hidden', !show_delete_all_btn); | |||||
} | } | ||||
get_selected() { | get_selected() { | ||||
@@ -835,10 +836,11 @@ export default class Grid { | |||||
$.each(row, (ci, value) => { | $.each(row, (ci, value) => { | ||||
var fieldname = fieldnames[ci]; | var fieldname = fieldnames[ci]; | ||||
var df = frappe.meta.get_docfield(me.df.options, fieldname); | var df = frappe.meta.get_docfield(me.df.options, fieldname); | ||||
d[fieldnames[ci]] = value_formatter_map[df.fieldtype] | |||||
? value_formatter_map[df.fieldtype](value) | |||||
: value; | |||||
if (df) { | |||||
d[fieldnames[ci]] = value_formatter_map[df.fieldtype] | |||||
? value_formatter_map[df.fieldtype](value) | |||||
: value; | |||||
} | |||||
}); | }); | ||||
} | } | ||||
} | } | ||||
@@ -483,7 +483,8 @@ frappe.ui.form.Layout = class Layout { | |||||
// next row | // next row | ||||
grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true); | grid_row.grid.grid_rows[grid_row.doc.idx].toggle_view(true); | ||||
} | } | ||||
} else { | |||||
} else if (!shift) { | |||||
// End of tab navigation | |||||
$(this.primary_button).focus(); | $(this.primary_button).focus(); | ||||
} | } | ||||
} | } | ||||
@@ -572,6 +572,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
render() { | render() { | ||||
this.render_list(); | this.render_list(); | ||||
this.set_rows_as_checked(); | |||||
this.on_row_checked(); | this.on_row_checked(); | ||||
this.render_count(); | this.render_count(); | ||||
} | } | ||||
@@ -607,9 +608,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
const subject_field = this.columns[0].df; | const subject_field = this.columns[0].df; | ||||
let subject_html = ` | let subject_html = ` | ||||
<input class="level-item list-check-all hidden-xs" type="checkbox" | |||||
<input class="level-item list-check-all" type="checkbox" | |||||
title="${__("Select All")}"> | title="${__("Select All")}"> | ||||
<span class="level-item list-liked-by-me"> | |||||
<span class="level-item list-liked-by-me hidden-xs"> | |||||
<span title="${__("Likes")}">${frappe.utils.icon('heart', 'sm', 'like-icon')}</span> | <span title="${__("Likes")}">${frappe.utils.icon('heart', 'sm', 'like-icon')}</span> | ||||
</span> | </span> | ||||
<span class="level-item">${__(subject_field.label)}</span> | <span class="level-item">${__(subject_field.label)}</span> | ||||
@@ -646,7 +647,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
</div> | </div> | ||||
<div class="level-left checkbox-actions"> | <div class="level-left checkbox-actions"> | ||||
<div class="level list-subject"> | <div class="level list-subject"> | ||||
<input class="level-item list-check-all hidden-xs" type="checkbox" | |||||
<input class="level-item list-check-all" type="checkbox" | |||||
title="${__("Select All")}"> | title="${__("Select All")}"> | ||||
<span class="level-item list-header-meta"></span> | <span class="level-item list-header-meta"></span> | ||||
</div> | </div> | ||||
@@ -954,9 +955,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||||
let subject_html = ` | let subject_html = ` | ||||
<span class="level-item select-like"> | <span class="level-item select-like"> | ||||
<input class="list-row-checkbox hidden-xs" type="checkbox" | |||||
<input class="list-row-checkbox" type="checkbox" | |||||
data-name="${escape(doc.name)}"> | data-name="${escape(doc.name)}"> | ||||
<span class="list-row-like style="margin-bottom: 1px;"> | |||||
<span class="list-row-like hidden-xs style="margin-bottom: 1px;"> | |||||
${this.get_like_html(doc)} | ${this.get_like_html(doc)} | ||||
</span> | </span> | ||||
</span> | </span> | ||||
@@ -927,7 +927,16 @@ Object.assign(frappe.utils, { | |||||
// decodes base64 to string | // decodes base64 to string | ||||
let parts = dataURI.split(','); | let parts = dataURI.split(','); | ||||
const encoded_data = parts[1]; | const encoded_data = parts[1]; | ||||
return decodeURIComponent(escape(atob(encoded_data))); | |||||
let decoded = atob(encoded_data); | |||||
try { | |||||
const escaped = escape(decoded); | |||||
decoded = decodeURIComponent(escaped); | |||||
} catch (e) { | |||||
// pass decodeURIComponent failure | |||||
// just return atob response | |||||
} | |||||
return decoded; | |||||
}, | }, | ||||
copy_to_clipboard(string) { | copy_to_clipboard(string) { | ||||
let input = $("<input>"); | let input = $("<input>"); | ||||
@@ -202,8 +202,12 @@ body { | |||||
} | } | ||||
// listviews | // listviews | ||||
.list-row { | |||||
padding: 13px 15px !important; | |||||
.select-like { | |||||
margin-right: unset !important; | |||||
} | |||||
.list-count { | |||||
display: contents; | |||||
} | } | ||||
.doclist-row { | .doclist-row { | ||||
@@ -1,2 +1,2 @@ | |||||
from pypika import * | from pypika import * | ||||
from frappe.query_builder.utils import get_query_builder, patch_query_execute | |||||
from frappe.query_builder.utils import Column, get_query_builder, patch_query_execute |
@@ -3,8 +3,10 @@ from typing import Any, Callable, Dict, get_type_hints | |||||
from importlib import import_module | from importlib import import_module | ||||
from pypika import Query | from pypika import Query | ||||
from pypika.queries import Column | |||||
import frappe | import frappe | ||||
from .builder import MariaDB, Postgres | from .builder import MariaDB, Postgres | ||||
@@ -1,31 +1,73 @@ | |||||
.hero-and-content { | .hero-and-content { | ||||
background-color: #f5f7fa; | |||||
background-color: var(--bg-color); | |||||
} | |||||
body { | |||||
background-color: var(--bg-color); | |||||
} | } | ||||
.page-card { | .page-card { | ||||
max-width: 360px; | max-width: 360px; | ||||
padding: 15px; | padding: 15px; | ||||
margin: 70px auto; | margin: 70px auto; | ||||
border: 1px solid #d1d8dd; | |||||
border-radius: 4px; | border-radius: 4px; | ||||
background-color: #fff; | |||||
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1); | |||||
background-color: var(--fg-color); | |||||
box-shadow: var(--shadow-base); | |||||
} | |||||
.for-reset-password { | |||||
margin: 80px 0; | |||||
} | |||||
.for-reset-password .page-card { | |||||
border: 0; | |||||
max-width: 450px; | |||||
margin: auto; | |||||
padding: 40px 60px; | |||||
border-radius: 10px; | |||||
box-shadow: var(--shadow-base); | |||||
} | } | ||||
.page-card .page-card-head { | .page-card .page-card-head { | ||||
padding: 10px 15px; | padding: 10px 15px; | ||||
margin: -15px; | margin: -15px; | ||||
margin-bottom: 15px; | margin-bottom: 15px; | ||||
border-bottom: 1px solid #d1d8dd; | |||||
border-bottom: 1px solid var(--border-color); | |||||
} | |||||
.for-reset-password .page-card .page-card-head { | |||||
border-bottom: 0; | |||||
} | |||||
.page-card-head h4 { | |||||
font-size: 18px; | |||||
font-weight: 600; | |||||
} | |||||
#reset-password .form-group { | |||||
margin-bottom: 10px; | |||||
font-size: var(--font-size-sm); | |||||
} | } | ||||
.page-card .page-card-head .indicator { | .page-card .page-card-head .indicator { | ||||
color: #36414C; | color: #36414C; | ||||
font-size: 14px; | font-size: 14px; | ||||
} | } | ||||
.sign-up-message { | |||||
margin-top: 20px; | |||||
font-size: 13px; | |||||
color: var(--text-color); | |||||
} | |||||
.page-card .page-card-head .indicator::before { | .page-card .page-card-head .indicator::before { | ||||
margin: 0 6px 0.5px 0px; | margin: 0 6px 0.5px 0px; | ||||
} | } | ||||
button#update { | |||||
font-size: var(--font-size-sm); | |||||
} | |||||
.page-card .btn { | .page-card .btn { | ||||
margin-top: 30px; | margin-top: 30px; | ||||
} | } | ||||
.page-card p { | .page-card p { | ||||
font-size: 14px; | font-size: 14px; | ||||
} | } | ||||
@@ -4,6 +4,7 @@ import frappe, unittest | |||||
from frappe.model.db_query import DatabaseQuery | from frappe.model.db_query import DatabaseQuery | ||||
from frappe.desk.reportview import get_filters_cond | from frappe.desk.reportview import get_filters_cond | ||||
from frappe.query_builder import Column | |||||
from frappe.core.page.permission_manager.permission_manager import update, reset, add | from frappe.core.page.permission_manager.permission_manager import update, reset, add | ||||
from frappe.permissions import add_user_permission, clear_user_permissions_for_doctype | from frappe.permissions import add_user_permission, clear_user_permissions_for_doctype | ||||
@@ -373,6 +374,25 @@ class TestReportview(unittest.TestCase): | |||||
owners = DatabaseQuery("DocType").execute(filters={"name": "DocType"}, pluck="owner") | owners = DatabaseQuery("DocType").execute(filters={"name": "DocType"}, pluck="owner") | ||||
self.assertEqual(owners, ["Administrator"]) | self.assertEqual(owners, ["Administrator"]) | ||||
def test_column_comparison(self): | |||||
"""Test DatabaseQuery.execute to test column comparison | |||||
""" | |||||
users_unedited = frappe.get_all( | |||||
"User", | |||||
filters={"creation": Column("modified")}, | |||||
fields=["name", "creation", "modified"], | |||||
limit=1, | |||||
) | |||||
users_edited = frappe.get_all( | |||||
"User", | |||||
filters={"creation": ("!=", Column("modified"))}, | |||||
fields=["name", "creation", "modified"], | |||||
limit=1, | |||||
) | |||||
self.assertEqual(users_unedited[0].modified, users_unedited[0].creation) | |||||
self.assertNotEqual(users_edited[0].modified, users_edited[0].creation) | |||||
def test_reportview_get(self): | def test_reportview_get(self): | ||||
user = frappe.get_doc("User", "test@example.com") | user = frappe.get_doc("User", "test@example.com") | ||||
add_child_table_to_blog_post() | add_child_table_to_blog_post() | ||||
@@ -9,9 +9,10 @@ from frappe.desk.form.utils import get_pdf_link | |||||
from frappe.utils.verified_command import get_signed_params, verify_request | from frappe.utils.verified_command import get_signed_params, verify_request | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.model.workflow import apply_workflow, get_workflow_name, has_approval_access, \ | from frappe.model.workflow import apply_workflow, get_workflow_name, has_approval_access, \ | ||||
get_workflow_state_field, send_email_alert, get_workflow_field_value, is_transition_condition_satisfied | |||||
get_workflow_state_field, send_email_alert, is_transition_condition_satisfied | |||||
from frappe.desk.notifications import clear_doctype_notifications | from frappe.desk.notifications import clear_doctype_notifications | ||||
from frappe.utils.user import get_users_with_role | from frappe.utils.user import get_users_with_role | ||||
from frappe.utils.data import get_link_to_form | |||||
class WorkflowAction(Document): | class WorkflowAction(Document): | ||||
pass | pass | ||||
@@ -287,12 +288,12 @@ def get_common_email_args(doc): | |||||
response = frappe.render_template(email_template.response, vars(doc)) | response = frappe.render_template(email_template.response, vars(doc)) | ||||
else: | else: | ||||
subject = _('Workflow Action') | subject = _('Workflow Action') | ||||
response = _('{0}: {1}').format(doctype, docname) | |||||
response = get_link_to_form(doctype, docname, f"{doctype}: {docname}") | |||||
common_args = { | common_args = { | ||||
'template': 'workflow_action', | 'template': 'workflow_action', | ||||
'header': 'Workflow Action', | 'header': 'Workflow Action', | ||||
'attachments': [frappe.attach_print(doctype, docname , file_name=docname)], | |||||
'attachments': [frappe.attach_print(doctype, docname, file_name=docname, doc=doc)], | |||||
'subject': subject, | 'subject': subject, | ||||
'message': response | 'message': response | ||||
} | } | ||||
@@ -1,32 +1,38 @@ | |||||
{% extends "templates/web.html" %} | {% extends "templates/web.html" %} | ||||
{% block title %} {{_("Reset Password")}} {% endblock %} | {% block title %} {{_("Reset Password")}} {% endblock %} | ||||
{% block head_include %} | |||||
{% endblock %} | |||||
{% block page_content %} | {% block page_content %} | ||||
<div class="page-card"> | |||||
<div class='page-card-head'> | |||||
<span class='indicator blue password-box'>{{ _("Reset Password") if frappe.db.get_default('company') else _("Set Password")}}</span> | |||||
</div> | |||||
<form id="reset-password"> | |||||
<div class="form-group" style="display: none;"> | |||||
<input id="old_password" type="password" | |||||
class="form-control" placeholder="{{ _("Old Password") }}"> | |||||
</div> | |||||
<div class="form-group"> | |||||
<input id="new_password" type="password" | |||||
class="form-control" placeholder="{{ _("New Password") }}"> | |||||
<span class="password-strength-indicator indicator"></span> | |||||
<section class="for-reset-password d-block"> | |||||
<div class="page-card"> | |||||
<div class='page-card-head text-center'> | |||||
<h4 class="reset-password-heading">{{ _("Reset Password") if frappe.db.get_default('company') else _("Set Password")}}</h4> | |||||
</div> | </div> | ||||
<p class='password-strength-message text-muted small hidden'></p> | |||||
<button type="submit" id="update" | |||||
class="btn btn-primary">{{_("Update")}}</button> | |||||
</form> | |||||
</div> | |||||
<form id="reset-password"> | |||||
<div class="form-group" style="display: none;"> | |||||
<input id="old_password" type="password" | |||||
class="form-control" placeholder="{{ _("Old Password") }}"> | |||||
</div> | |||||
<div class="form-group"> | |||||
<input id="new_password" type="password" | |||||
class="form-control" placeholder="{{ _("New Password") }}"> | |||||
<span class="password-strength-indicator indicator"></span> | |||||
</div> | |||||
<p class='password-strength-message text-muted small hidden'></p> | |||||
<button type="submit" id="update" | |||||
class="btn btn-primary btn-block btn-update">{{_("Confirm")}}</button> | |||||
</form> | |||||
{%- if not disable_signup -%} | |||||
<div class="text-center sign-up-message"> | |||||
{{ _("Don't have an account?") }} | |||||
<a href="/login#signup">{{ _("Sign up") }}</a> | |||||
</div> | |||||
{%- endif -%} | |||||
</div> | |||||
</section> | |||||
<style> | <style> | ||||
.hero-and-content { | |||||
background-color: #f5f7fa; | |||||
} | |||||
</style> | </style> | ||||
<script> | <script> | ||||
@@ -69,20 +75,19 @@ frappe.ready(function() { | |||||
args: args, | args: args, | ||||
statusCode: { | statusCode: { | ||||
401: function() { | 401: function() { | ||||
$(".page-card-head .indicator").removeClass().addClass("indicator red").text(__("Invalid Password")); | |||||
$(".page-card-head .reset-password-heading").text(__("Invalid Password")); | |||||
}, | }, | ||||
410: function({ responseJSON }) { | 410: function({ responseJSON }) { | ||||
const title = __("Invalid Link"); | const title = __("Invalid Link"); | ||||
const message = responseJSON.message; | const message = responseJSON.message; | ||||
$(".page-card-head .indicator").removeClass().addClass("indicator grey").text(title); | |||||
$(".page-card-head .reset-password-heading").text(title); | |||||
frappe.msgprint({ title: title, message: message, clear: true }); | frappe.msgprint({ title: title, message: message, clear: true }); | ||||
}, | }, | ||||
200: function(r) { | 200: function(r) { | ||||
$("input").val(""); | $("input").val(""); | ||||
strength_indicator.addClass("hidden"); | strength_indicator.addClass("hidden"); | ||||
strength_message.addClass("hidden"); | strength_message.addClass("hidden"); | ||||
$(".page-card-head .indicator") | |||||
.removeClass().addClass("indicator blue") | |||||
$(".page-card-head .reset-password-heading") | |||||
.html(__("Status Updated")); | .html(__("Status Updated")); | ||||
if(r.message) { | if(r.message) { | ||||
frappe.msgprint({ | frappe.msgprint({ | ||||
@@ -132,7 +137,7 @@ frappe.ready(function() { | |||||
}, | }, | ||||
statusCode: { | statusCode: { | ||||
401: function() { | 401: function() { | ||||
$('.page-card-head .indicator').removeClass().addClass('indicator red') | |||||
$('.page-card-head .reset-password-heading') | |||||
.text("{{ _('Invalid Password') }}"); | .text("{{ _('Invalid Password') }}"); | ||||
}, | }, | ||||
200: function(r) { | 200: function(r) { | ||||
@@ -175,7 +180,6 @@ frappe.ready(function() { | |||||
} | } | ||||
} | } | ||||
strength_indicator.removeClass().addClass('password-strength-indicator indicator ' + color); | |||||
strength_message.html(message.join(' ') || '').removeClass('hidden'); | strength_message.html(message.join(' ') || '').removeClass('hidden'); | ||||
// strength_indicator.attr('title', message.join(' ') || ''); | // strength_indicator.attr('title', message.join(' ') || ''); | ||||
} | } | ||||
@@ -24,8 +24,7 @@ googlemaps~=4.4.5 | |||||
gunicorn~=20.1.0 | gunicorn~=20.1.0 | ||||
html2text==2020.1.16 | html2text==2020.1.16 | ||||
html5lib~=1.1 | html5lib~=1.1 | ||||
ipython~=7.16.1 | |||||
jedi==0.17.2 # not directly required. Pinned to fix upstream IPython issue (https://github.com/ipython/ipython/issues/12740) | |||||
ipython~=7.27.0 | |||||
Jinja2~=3.0.1 | Jinja2~=3.0.1 | ||||
ldap3~=2.9 | ldap3~=2.9 | ||||
markdown2~=2.4.0 | markdown2~=2.4.0 | ||||
@@ -57,5 +57,5 @@ setup( | |||||
{ | { | ||||
'clean': CleanCommand | 'clean': CleanCommand | ||||
}, | }, | ||||
python_requires='>=3.6' | |||||
python_requires='>=3.7' | |||||
) | ) |