Merge branch 'develop' into wspace-new-design

This commit is contained in:
Shariq Ansari 2022-01-11 17:31:27 +05:30 committed by GitHub
commit 8dcf416eb6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 944 additions and 326 deletions

View file

@ -148,6 +148,7 @@
"context": true,
"before": true,
"beforeEach": true,
"after": true,
"qz": true,
"localforage": true,
"extend_cscript": true

View file

@ -12,7 +12,7 @@ jobs:
- name: 'Setup Environment'
uses: actions/setup-python@v2
with:
python-version: 3.7
python-version: 3.8
- name: 'Clone repo'
uses: actions/checkout@v2

View file

@ -0,0 +1,45 @@
context("First Day of the Week", () => {
before(() => {
cy.login();
});
beforeEach(() => {
cy.visit('/app/system-settings');
cy.findByText('Date and Number Format').click();
});
it("Date control starts with same day as selected in System Settings", () => {
cy.intercept('POST', '/api/method/frappe.core.doctype.system_settings.system_settings.load').as("load_settings");
cy.fill_field('first_day_of_the_week', 'Tuesday', 'Select');
cy.findByRole('button', {name: 'Save'}).click();
cy.wait("@load_settings");
cy.dialog({
title: 'Date',
fields: [
{
label: 'Date',
fieldname: 'date',
fieldtype: 'Date'
}
]
});
cy.get_field('date').click();
cy.get('.datepicker--day-name').eq(0).should('have.text', 'Tu');
});
it("Calendar view starts with same day as selected in System Settings", () => {
cy.intercept('POST', '/api/method/frappe.core.doctype.system_settings.system_settings.load').as("load_settings");
cy.fill_field('first_day_of_the_week', 'Monday', 'Select');
cy.findByRole('button', {name: 'Save'}).click();
cy.wait("@load_settings");
cy.visit("app/todo/view/calendar/default");
cy.get('.fc-day-header > span').eq(0).should('have.text', 'Mon');
});
after(() => {
cy.visit('/app/system-settings');
cy.findByText('Date and Number Format').click();
cy.fill_field('first_day_of_the_week', 'Sunday', 'Select');
cy.findByRole('button', {name: 'Save'}).click();
});
});

View file

@ -1,48 +1,39 @@
context('Grid Keyboard Shortcut', () => {
let total_count = 0;
beforeEach(() => {
cy.login();
cy.visit('/app/doctype/User');
});
before(() => {
cy.login();
cy.visit('/app/doctype/User');
return cy.window().its('frappe').then(frappe => {
frappe.db.count('DocField', {
filters: {
'parent': 'User', 'parentfield': 'fields', 'parenttype': 'DocType'
}
}).then((r) => {
total_count = r;
});
});
});
beforeEach(() => {
cy.reload();
cy.visit('/app/contact/new-contact-1');
cy.get('.frappe-control[data-fieldname="email_ids"]').find(".grid-add-row").click();
});
it('Insert new row at the end', () => {
cy.add_new_row_in_grid('{ctrl}{shift}{downarrow}', (cy, total_count) => {
cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', `${total_count+1}`);
cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', `${total_count+1}`);
}, total_count);
});
it('Insert new row at the top', () => {
cy.add_new_row_in_grid('{ctrl}{shift}{uparrow}', (cy) => {
cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', '1');
cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', '2');
});
});
it('Insert new row below', () => {
cy.add_new_row_in_grid('{ctrl}{downarrow}', (cy) => {
cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', '2');
cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', '1');
});
});
it('Insert new row above', () => {
cy.add_new_row_in_grid('{ctrl}{uparrow}', (cy) => {
cy.get('[data-name="new-docfield-1"]').should('have.attr', 'data-idx', '1');
cy.get('[data-name="new-contact-email-1"]').should('have.attr', 'data-idx', '2');
});
});
});
Cypress.Commands.add('add_new_row_in_grid', (shortcut_keys, callbackFn, total_count) => {
cy.get('.frappe-control[data-fieldname="fields"]').as('table');
cy.get('@table').find('.grid-body .col-xs-2').first().click();
cy.get('@table').find('.grid-body .col-xs-2')
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
cy.get('@table').find('.grid-body [data-fieldname="email_id"]').first().click();
cy.get('@table').find('.grid-body [data-fieldname="email_id"]')
.first().type(shortcut_keys);
callbackFn(cy, total_count);

View file

@ -0,0 +1,29 @@
context('Web Form', () => {
before(() => {
cy.login();
});
it('Navigate and Submit a WebForm', () => {
cy.visit('/update-profile');
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
cy.get('.web-form-actions .btn-primary').click();
cy.wait(500);
cy.get('.modal.show > .modal-dialog').should('be.visible');
});
it('Navigate and Submit a MultiStep WebForm', () => {
cy.call('frappe.tests.ui_test_helpers.update_webform_to_multistep').then(() => {
cy.visit('/update-profile-duplicate');
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
cy.get('.btn-next').should('be.visible');
cy.get('.web-form-footer .btn-primary').should('not.be.visible');
cy.get('.btn-next').click();
cy.get('.btn-previous').should('be.visible');
cy.get('.btn-next').should('not.be.visible');
cy.get('.web-form-footer .btn-primary').should('be.visible');
cy.get('.web-form-actions .btn-primary').click();
cy.wait(500);
cy.get('.modal.show > .modal-dialog').should('be.visible');
});
});
});

View file

@ -193,7 +193,8 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
});
Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
let selector = `[data-fieldname="${fieldname}"] input:visible`;
let field_element = fieldtype === 'Select' ? 'select': 'input';
let selector = `[data-fieldname="${fieldname}"] ${field_element}:visible`;
if (fieldtype === 'Text Editor') {
selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]:visible`;

View file

@ -1,32 +1,47 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# Copyright (c) 2022, Frappe Technologies and contributors
# License: MIT. See LICENSE
from typing import Dict, Iterable, List
import frappe
from frappe.model.document import Document
from frappe.desk.form import assign_to
import frappe.cache_manager
from frappe import _
from frappe.cache_manager import clear_doctype_map, get_doctype_map
from frappe.desk.form import assign_to
from frappe.model import log_types
from frappe.model.document import Document
class AssignmentRule(Document):
def validate(self):
self.validate_document_types()
self.validate_assignment_days()
def clear_cache(self):
super().clear_cache()
clear_doctype_map(self.doctype, self.document_type)
clear_doctype_map(self.doctype, f"due_date_rules_for_{self.document_type}")
def validate_document_types(self):
if self.document_type == "ToDo":
frappe.throw(
_('Assignment Rule is not allowed on {0} document type').format(
frappe.bold("ToDo")
)
)
def validate_assignment_days(self):
assignment_days = self.get_assignment_days()
if not len(set(assignment_days)) == len(assignment_days):
if len(set(assignment_days)) != len(assignment_days):
repeated_days = get_repeated(assignment_days)
frappe.throw(_("Assignment Day {0} has been repeated.").format(frappe.bold(repeated_days)))
if self.document_type == 'ToDo':
frappe.throw(_('Assignment Rule is not allowed on {0} document type').format(frappe.bold("ToDo")))
plural = "s" if len(repeated_days) > 1 else ""
def on_update(self):
clear_assignment_rule_cache(self)
def after_rename(self, old, new, merge):
clear_assignment_rule_cache(self)
def on_trash(self):
clear_assignment_rule_cache(self)
frappe.throw(
_("Assignment Day{0} {1} has been repeated.").format(
plural,
frappe.bold(", ".join(repeated_days))
)
)
def apply_unassign(self, doc, assignments):
if (self.unassign_condition and
@ -35,7 +50,6 @@ class AssignmentRule(Document):
return False
def apply_assign(self, doc):
if self.safe_eval('assign_condition', doc):
return self.do_assignment(doc)
@ -141,65 +155,68 @@ class AssignmentRule(Document):
def is_rule_not_applicable_today(self):
today = frappe.flags.assignment_day or frappe.utils.get_weekday()
assignment_days = self.get_assignment_days()
if assignment_days and not today in assignment_days:
return True
return assignment_days and today not in assignment_days
return False
def get_assignments(doc):
def get_assignments(doc) -> List[Dict]:
return frappe.get_all('ToDo', fields = ['name', 'assignment_rule'], filters = dict(
reference_type = doc.get('doctype'),
reference_name = doc.get('name'),
status = ('!=', 'Cancelled')
), limit = 5)
), limit=5)
@frappe.whitelist()
def bulk_apply(doctype, docnames):
import json
docnames = json.loads(docnames)
docnames = frappe.parse_json(docnames)
background = len(docnames) > 5
for name in docnames:
if background:
frappe.enqueue('frappe.automation.doctype.assignment_rule.assignment_rule.apply', doc=None, doctype=doctype, name=name)
else:
apply(None, doctype=doctype, name=name)
apply(doctype=doctype, name=name)
def reopen_closed_assignment(doc):
todo_list = frappe.db.get_all('ToDo', filters = dict(
reference_type = doc.doctype,
reference_name = doc.name,
status = 'Closed'
))
if not todo_list:
return False
todo_list = frappe.get_all("ToDo", filters={
"reference_type": doc.doctype,
"reference_name": doc.name,
"status": "Closed",
}, pluck="name")
for todo in todo_list:
todo_doc = frappe.get_doc('ToDo', todo.name)
todo_doc = frappe.get_doc('ToDo', todo)
todo_doc.status = 'Open'
todo_doc.save(ignore_permissions=True)
return True
def apply(doc, method=None, doctype=None, name=None):
if not doctype:
doctype = doc.doctype
return bool(todo_list)
if (frappe.flags.in_patch
def apply(doc=None, method=None, doctype=None, name=None):
doctype = doctype or doc.doctype
skip_assignment_rules = (
frappe.flags.in_patch
or frappe.flags.in_install
or frappe.flags.in_setup_wizard
or doctype in log_types):
or doctype in log_types
)
if skip_assignment_rules:
return
if not doc and doctype and name:
doc = frappe.get_doc(doctype, name)
assignment_rules = frappe.cache_manager.get_doctype_map('Assignment Rule', doc.doctype, dict(
document_type = doc.doctype, disabled = 0), order_by = 'priority desc')
assignment_rule_docs = []
assignment_rules = get_doctype_map("Assignment Rule", doc.doctype, filters={
"document_type": doc.doctype, "disabled": 0
}, order_by="priority desc")
# multiple auto assigns
for d in assignment_rules:
assignment_rule_docs.append(frappe.get_cached_doc('Assignment Rule', d.get('name')))
assignment_rule_docs: List[AssignmentRule] = [
frappe.get_cached_doc("Assignment Rule", d.get('name')) for d in assignment_rules
]
if not assignment_rule_docs:
return
@ -235,6 +252,7 @@ def apply(doc, method=None, doctype=None, name=None):
# apply close rule only if assignments exists
assignments = get_assignments(doc)
if assignments:
for assignment_rule in assignment_rule_docs:
if assignment_rule.is_rule_not_applicable_today():
@ -242,38 +260,74 @@ def apply(doc, method=None, doctype=None, name=None):
if not new_apply:
# only reopen if close condition is not satisfied
if not assignment_rule.safe_eval('close_condition', doc):
reopen = reopen_closed_assignment(doc)
if reopen:
to_close_todos = assignment_rule.safe_eval('close_condition', doc)
if to_close_todos:
# close todo status
todos_to_close = frappe.get_all("ToDo", filters={
"reference_type": doc.doctype,
"reference_name": doc.name,
}, pluck="name")
for todo in todos_to_close:
_todo = frappe.get_doc("ToDo", todo)
_todo.status = "Closed"
_todo.save()
break
else:
reopened = reopen_closed_assignment(doc)
if reopened:
break
# print(f"Rule:{assignment_rule}\nDoc: {doc}\nReOpened: {reopened}")
assignment_rule.close_assignments(doc)
def update_due_date(doc, state=None):
# called from hook
if (frappe.flags.in_patch
or frappe.flags.in_install
or frappe.flags.in_migrate
"""Run on_update on every Document (via hooks.py)
"""
skip_document_update = (
frappe.flags.in_migrate
or frappe.flags.in_patch
or frappe.flags.in_import
or frappe.flags.in_setup_wizard):
or frappe.flags.in_setup_wizard
or frappe.flags.in_install
)
if skip_document_update:
return
assignment_rules = frappe.cache_manager.get_doctype_map('Assignment Rule', 'due_date_rules_for_' + doc.doctype, dict(
document_type = doc.doctype,
disabled = 0,
due_date_based_on = ['is', 'set']
))
assignment_rules = get_doctype_map(
doctype="Assignment Rule",
name=f"due_date_rules_for_{doc.doctype}",
filters={
"due_date_based_on": ["is", "set"],
"document_type": doc.doctype,
"disabled": 0,
}
)
for rule in assignment_rules:
rule_doc = frappe.get_cached_doc('Assignment Rule', rule.get('name'))
rule_doc = frappe.get_cached_doc("Assignment Rule", rule.get("name"))
due_date_field = rule_doc.due_date_based_on
if doc.meta.has_field(due_date_field) and \
doc.has_value_changed(due_date_field) and rule.get('name'):
assignment_todos = frappe.get_all('ToDo', {
'assignment_rule': rule.get('name'),
'status': 'Open',
'reference_type': doc.doctype,
'reference_name': doc.name
})
field_updated = (
doc.meta.has_field(due_date_field)
and doc.has_value_changed(due_date_field)
and rule.get("name")
)
if field_updated:
assignment_todos = frappe.get_all("ToDo", filters={
"assignment_rule": rule.get("name"),
"reference_type": doc.doctype,
"reference_name": doc.name,
"status": "Open",
}, pluck="name")
for todo in assignment_todos:
todo_doc = frappe.get_doc('ToDo', todo.name)
todo_doc = frappe.get_doc('ToDo', todo)
todo_doc.date = doc.get(due_date_field)
todo_doc.flags.updater_reference = {
'doctype': 'Assignment Rule',
@ -282,20 +336,19 @@ def update_due_date(doc, state=None):
}
todo_doc.save(ignore_permissions=True)
def get_assignment_rules():
return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))]
def get_repeated(values):
unique_list = []
diff = []
def get_assignment_rules() -> List[str]:
return frappe.get_all("Assignment Rule", filters={"disabled": 0}, pluck="document_type")
def get_repeated(values: Iterable) -> List:
unique = set()
repeated = set()
for value in values:
if value not in unique_list:
unique_list.append(str(value))
if value in unique:
repeated.add(value)
else:
if value not in diff:
diff.append(str(value))
return " ".join(diff)
unique.add(value)
def clear_assignment_rule_cache(rule):
frappe.cache_manager.clear_doctype_map('Assignment Rule', rule.document_type)
frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type)
return [str(x) for x in repeated]

View file

@ -1,12 +1,22 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# Copyright (c) 2021, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import frappe
import unittest
from frappe.utils import random_string
import frappe
from frappe.test_runner import make_test_records
from frappe.utils import random_string
class TestAutoAssign(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.db.delete("Assignment Rule")
@classmethod
def tearDownClass(cls):
frappe.db.rollback()
def setUp(self):
make_test_records("User")
days = [
@ -129,7 +139,7 @@ class TestAutoAssign(unittest.TestCase):
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
))[0]
), limit=1)[0]
todo = frappe.get_doc('ToDo', todo['name'])
self.assertEqual(todo.allocated_to, 'test@example.com')
@ -151,7 +161,7 @@ class TestAutoAssign(unittest.TestCase):
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
))[0]
), limit=1)[0]
todo = frappe.get_doc('ToDo', todo['name'])
self.assertEqual(todo.allocated_to, 'test@example.com')

View file

@ -96,7 +96,15 @@ class AutoRepeat(Document):
auto_repeat_days = self.get_auto_repeat_days()
if not len(set(auto_repeat_days)) == len(auto_repeat_days):
repeated_days = get_repeated(auto_repeat_days)
frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days)))
plural = "s" if len(repeated_days) > 1 else ""
frappe.throw(
_("Auto Repeat Day{0} {1} has been repeated.").format(
plural,
frappe.bold(", ".join(repeated_days))
)
)
def update_auto_repeat_id(self):
#check if document is already on auto repeat

View file

@ -12,7 +12,7 @@ from frappe.core.utils import get_parent_doc
from frappe.utils.bot import BotReply
from frappe.utils import parse_addr, split_emails
from frappe.core.doctype.comment.comment import update_comment_in_doc
from email.utils import parseaddr
from email.utils import getaddresses
from urllib.parse import unquote
from frappe.utils.user import is_system_user
from frappe.contacts.doctype.contact.contact import get_contact_name
@ -372,10 +372,9 @@ def get_contacts(email_strings, auto_create_contact=False):
for email_string in email_strings:
if email_string:
for email in email_string.split(","):
parsed_email = parseaddr(email)[1]
if parsed_email:
email_addrs.append(parsed_email)
result = getaddresses([email_string])
for email in result:
email_addrs.append(email[1])
contacts = []
for email in email_addrs:
@ -488,10 +487,12 @@ def update_parent_document_on_communication(doc):
def update_first_response_time(parent, communication):
if parent.meta.has_field("first_response_time") and not parent.get("first_response_time"):
if is_system_user(communication.sender):
first_responded_on = communication.creation
if parent.meta.has_field("first_responded_on") and communication.sent_or_received == "Sent":
parent.db_set("first_responded_on", first_responded_on)
parent.db_set("first_response_time", round(time_diff_in_seconds(first_responded_on, parent.creation), 2))
if communication.sent_or_received == "Sent":
first_responded_on = communication.creation
if parent.meta.has_field("first_responded_on"):
parent.db_set("first_responded_on", first_responded_on)
first_response_time = round(time_diff_in_seconds(first_responded_on, parent.creation), 2)
parent.db_set("first_response_time", first_response_time)
def set_avg_response_time(parent, communication):
if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent":

View file

@ -1283,7 +1283,7 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
roles = [p.role for p in doc.get("permissions") or []] + default_roles
for role in list(set(roles)):
if not frappe.db.exists("Role", role):
if frappe.db.table_exists("Role", cached=False) and not frappe.db.exists("Role", role):
r = frappe.get_doc(dict(doctype= "Role", role_name=role, desk_access=1))
r.flags.ignore_mandatory = r.flags.ignore_permissions = True
r.insert()

View file

@ -15,6 +15,10 @@ from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError,
# test_records = frappe.get_test_records('DocType')
class TestDocType(unittest.TestCase):
def tearDown(self):
frappe.db.rollback()
def test_validate_name(self):
self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert)
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert)
@ -42,6 +46,7 @@ class TestDocType(unittest.TestCase):
doc1.insert()
self.assertRaises(frappe.UniqueValidationError, doc2.insert)
frappe.db.rollback()
dt.fields[0].unique = 0
dt.save()

View file

@ -10,6 +10,10 @@ frappe.ui.form.on("System Settings", {
frm.set_value(key, val);
frappe.sys_defaults[key] = val;
});
if (frm.re_setup_moment) {
frappe.app.setup_moment();
delete frm.re_setup_moment;
}
}
});
},
@ -38,5 +42,8 @@ frappe.ui.form.on("System Settings", {
// Clear cache after saving to refresh the values of boot.
frappe.ui.toolbar.clear_cache();
}
}
},
first_day_of_the_week(frm) {
frm.re_setup_moment = true;
},
});

View file

@ -17,10 +17,11 @@
"date_and_number_format",
"date_format",
"time_format",
"column_break_7",
"number_format",
"column_break_7",
"float_precision",
"currency_precision",
"first_day_of_the_week",
"sec_backup_limit",
"backup_limit",
"encrypt_backup",
@ -477,12 +478,19 @@
"fieldname": "disable_system_update_notification",
"fieldtype": "Check",
"label": "Disable System Update Notification"
},
{
"default": "Sunday",
"fieldname": "first_day_of_the_week",
"fieldtype": "Select",
"label": "First Day of the Week",
"options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2021-11-29 18:09:53.601629",
"modified": "2022-01-04 11:28:34.881192",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
@ -499,5 +507,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View file

@ -37,16 +37,14 @@ class UserType(Document):
return
modules = frappe.get_all("DocType",
fields=["module"],
filters={"name": ("in", [d.document_type for d in self.user_doctypes])},
distinct=True,
pluck="module",
)
self.set('user_type_modules', [])
for row in modules:
self.append('user_type_modules', {
'module': row.module
})
self.set("user_type_modules", [])
for module in modules:
self.append("user_type_modules", {"module": module})
def validate_document_type_limit(self):
limit = frappe.conf.get('user_type_doctype_limit', {}).get(frappe.scrub(self.name))

View file

@ -164,10 +164,7 @@ class Database(object):
frappe.errprint(("Execution time: {0} sec").format(round(time_end - time_start, 2)))
except Exception as e:
if frappe.conf.db_type == 'postgres':
self.rollback()
elif self.is_syntax_error(e):
if self.is_syntax_error(e):
# only for mariadb
frappe.errprint('Syntax error in query:')
frappe.errprint(query)
@ -178,6 +175,9 @@ class Database(object):
elif self.is_timedout(e):
raise frappe.QueryTimeoutError(e)
elif frappe.conf.db_type == 'postgres':
raise
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
pass
else:
@ -267,9 +267,7 @@ class Database(object):
"""Raises exception if more than 20,000 `INSERT`, `UPDATE` queries are
executed in one transaction. This is to ensure that writes are always flushed otherwise this
could cause the system to hang."""
if self.transaction_writes and \
query and query.strip().split()[0].lower() in ['start', 'alter', 'drop', 'create', "begin", "truncate"]:
raise Exception('This statement can cause implicit commit')
self.check_implicit_commit(query)
if query and query.strip().lower() in ('commit', 'rollback'):
self.transaction_writes = 0
@ -282,6 +280,11 @@ class Database(object):
else:
frappe.throw(_("Too many writes in one request. Please send smaller requests"), frappe.ValidationError)
def check_implicit_commit(self, query):
if self.transaction_writes and \
query and query.strip().split()[0].lower() in ['start', 'alter', 'drop', 'create', "begin", "truncate"]:
raise Exception('This statement can cause implicit commit')
def fetch_as_dict(self, formatted=0, as_utf8=0):
"""Internal. Converts results to dict."""
result = self._cursor.fetchall()
@ -701,6 +704,8 @@ class Database(object):
self.sql("""update `tab{0}`
set {1} where name=%(name)s""".format(dt, ', '.join(set_values)),
values, debug=debug)
frappe.clear_document_cache(dt, values['name'])
else:
# for singles
keys = list(to_update)
@ -713,10 +718,11 @@ class Database(object):
self.sql('''insert into `tabSingles` (doctype, field, value) values (%s, %s, %s)''',
(dt, key, value), debug=debug)
frappe.clear_document_cache(dt, dn)
if dt in self.value_cache:
del self.value_cache[dt]
frappe.clear_document_cache(dt, dn)
@staticmethod
def set(doc, field, val):
@ -835,9 +841,9 @@ class Database(object):
'parent': dt
})
def table_exists(self, doctype):
def table_exists(self, doctype, cached=True):
"""Returns True if table for given doctype exists."""
return ("tab" + doctype) in self.get_tables()
return ("tab" + doctype) in self.get_tables(cached=cached)
def has_table(self, doctype):
return self.table_exists(doctype)

View file

@ -3,7 +3,7 @@ from typing import List, Tuple, Union
import psycopg2
import psycopg2.extensions
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ
from psycopg2.errorcodes import STRING_DATA_RIGHT_TRUNCATION
import frappe
@ -69,14 +69,20 @@ class PostgresDatabase(Database):
conn = psycopg2.connect("host='{}' dbname='{}' user='{}' password='{}' port={}".format(
self.host, self.user, self.user, self.password, self.port
))
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) # TODO: Remove this
conn.set_isolation_level(ISOLATION_LEVEL_REPEATABLE_READ)
return conn
def escape(self, s, percent=True):
"""Excape quotes and percent in given string."""
"""Escape quotes and percent in given string."""
if isinstance(s, bytes):
s = s.decode('utf-8')
# MariaDB's driver treats None as an empty string
# So Postgres should do the same
if s is None:
s = ''
if percent:
s = s.replace("%", "%%")
@ -103,7 +109,7 @@ class PostgresDatabase(Database):
return super(PostgresDatabase, self).sql(*args, **kwargs)
def get_tables(self):
def get_tables(self, cached=True):
return [d[0] for d in self.sql("""select table_name
from information_schema.tables
where table_catalog='{0}'
@ -138,6 +144,10 @@ class PostgresDatabase(Database):
# http://initd.org/psycopg/docs/extensions.html?highlight=datatype#psycopg2.extensions.QueryCanceledError
return isinstance(e, psycopg2.extensions.QueryCanceledError)
@staticmethod
def is_syntax_error(e):
return isinstance(e, psycopg2.errors.SyntaxError)
@staticmethod
def is_table_missing(e):
return getattr(e, 'pgcode', None) == '42P01'
@ -255,8 +265,8 @@ class PostgresDatabase(Database):
key=key
)
def check_transaction_status(self, query):
pass
def check_implicit_commit(self, query):
pass # postgres can run DDL in transactions without implicit commits
def has_index(self, table_name, index_name):
return self.sql("""SELECT 1 FROM pg_indexes WHERE tablename='{table_name}'

View file

@ -206,6 +206,12 @@ class DbColumn:
if not current_def:
self.fieldname = validate_column_name(self.fieldname)
self.table.add_column.append(self)
if column_type not in ('text', 'longtext'):
if self.unique:
self.table.add_unique.append(self)
if self.set_index:
self.table.add_index.append(self)
return
# type

View file

@ -7,6 +7,7 @@ from frappe.model.document import Document
from frappe import _
from frappe.utils import cint
class BulkUpdate(Document):
pass
@ -22,7 +23,7 @@ def update(doctype, field, value, condition='', limit=500):
frappe.throw(_('; not allowed in condition'))
docnames = frappe.db.sql_list(
'''select name from `tab{0}`{1} limit 0, {2}'''.format(doctype, condition, limit)
'''select name from `tab{0}`{1} limit {2} offset 0'''.format(doctype, condition, limit)
)
data = {}
data[field] = value

View file

@ -1,23 +1,33 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# Copyright (c) 2022, Frappe Technologies and contributors
# License: MIT. See LICENSE
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
from frappe.config import get_modules_from_all_apps_for_user
import json
import frappe
from frappe import _
import json
from frappe.config import get_modules_from_all_apps_for_user
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
from frappe.query_builder import DocType
class Dashboard(Document):
def on_update(self):
if self.is_default:
# make all other dashboards non-default
frappe.db.sql('''update
tabDashboard set is_default = 0 where name != %s''', self.name)
DashBoard = DocType("Dashboard")
frappe.qb.update(DashBoard).set(
DashBoard.is_default, 0
).where(
DashBoard.name != self.name
).run()
if frappe.conf.developer_mode and self.is_standard:
export_to_files(record_list=[['Dashboard', self.name, self.module + ' Dashboard']], record_module=self.module)
export_to_files(
record_list=[["Dashboard", self.name, f"{self.module} Dashboard"]],
record_module=self.module
)
def validate(self):
if not frappe.conf.developer_mode and self.is_standard:

View file

@ -8,6 +8,7 @@ from frappe.desk.doctype.dashboard_chart.dashboard_chart import get
from datetime import datetime
from dateutil.relativedelta import relativedelta
from unittest.mock import patch
class TestDashboardChart(unittest.TestCase):
def test_period_ending(self):
@ -15,8 +16,9 @@ class TestDashboardChart(unittest.TestCase):
getdate('2019-04-10'))
# week starts on monday
self.assertEqual(get_period_ending('2019-04-10', 'Weekly'),
getdate('2019-04-14'))
with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"):
self.assertEqual(get_period_ending('2019-04-10', 'Weekly'),
getdate('2019-04-14'))
self.assertEqual(get_period_ending('2019-04-10', 'Monthly'),
getdate('2019-04-30'))
@ -200,13 +202,14 @@ class TestDashboardChart(unittest.TestCase):
timeseries = 1
)).insert()
result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1)
with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"):
result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1)
self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0])
self.assertEqual(
result.get('labels'),
['30-12-18', '06-01-19', '13-01-19', '20-01-19']
)
self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0])
self.assertEqual(
result.get('labels'),
['30-12-18', '06-01-19', '13-01-19', '20-01-19']
)
frappe.db.rollback()
@ -231,13 +234,13 @@ class TestDashboardChart(unittest.TestCase):
timeseries = 1
)).insert()
result = get(chart_name='Test Average Dashboard Chart', refresh = 1)
self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 150.0, 266.6666666666667, 0.0])
self.assertEqual(
result.get('labels'),
['30-12-18', '06-01-19', '13-01-19', '20-01-19']
)
with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"):
result = get(chart_name='Test Average Dashboard Chart', refresh = 1)
self.assertEqual(
result.get('labels'),
['30-12-18', '06-01-19', '13-01-19', '20-01-19']
)
self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 150.0, 266.6666666666667, 0.0])
frappe.db.rollback()

View file

@ -100,5 +100,5 @@ frappe.ui.form.on('System Console', {
</tr></thead>
<tbody>${rows}</thead>`);
});
}
},
});

View file

@ -69,15 +69,13 @@ class ToDo(Document):
return
try:
assignments = [d[0] for d in frappe.get_all("ToDo",
filters={
"reference_type": self.reference_type,
"reference_name": self.reference_name,
"status": ("!=", "Cancelled")
},
fields=["allocated_to"], as_list=True)]
assignments = frappe.get_all("ToDo", filters={
"reference_type": self.reference_type,
"reference_name": self.reference_name,
"status": ("!=", "Cancelled")
}, pluck="allocated_to")
assignments.reverse()
frappe.db.set_value(self.reference_type, self.reference_name,
"_assign", json.dumps(assignments), update_modified=False)

View file

@ -19,11 +19,11 @@ def get(args=None):
if not args:
args = frappe.local.form_dict
return frappe.get_all('ToDo', fields=['allocated_to', 'name'], filters=dict(
reference_type = args.get('doctype'),
reference_name = args.get('name'),
status = ('!=', 'Cancelled')
), limit=5)
return frappe.get_all("ToDo", fields=["allocated_to as owner", "name"], filters={
"reference_type": args.get("doctype"),
"reference_name": args.get("name"),
"status": ("!=", "Cancelled")
}, limit=5)
@frappe.whitelist()
def add(args=None):

View file

@ -253,7 +253,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
def get_assignments(dt, dn):
cl = frappe.get_all("ToDo",
fields=['name', 'owner', 'description', 'status'],
fields=['name', 'allocated_to as owner', 'description', 'status'],
filters={
'reference_type': dt,
'reference_name': dn,

View file

@ -388,7 +388,6 @@ def make_records(records, debug=False):
# LOG every success and failure
for record in records:
doctype = record.get("doctype")
condition = record.get('__condition')
@ -405,6 +404,7 @@ def make_records(records, debug=False):
try:
doc.insert(ignore_permissions=True)
frappe.db.commit()
except frappe.DuplicateEntryError as e:
# print("Failed to insert duplicate {0} {1}".format(doctype, doc.name))
@ -417,6 +417,7 @@ def make_records(records, debug=False):
raise
except Exception as e:
frappe.db.rollback()
exception = record.get('__exception')
if exception:
config = _dict(exception)

View file

@ -4,6 +4,7 @@
import frappe
from frappe import _
@frappe.whitelist()
def get_all_nodes(doctype, label, parent, tree_method, **filters):
'''Recursively gets all data from tree nodes'''
@ -40,8 +41,8 @@ def get_children(doctype, parent='', **filters):
def _get_children(doctype, parent='', ignore_permissions=False):
parent_field = 'parent_' + doctype.lower().replace(' ', '_')
filters = [['ifnull(`{0}`,"")'.format(parent_field), '=', parent],
['docstatus', '<' ,'2']]
filters = [["ifnull(`{0}`,'')".format(parent_field), '=', parent],
['docstatus', '<' ,2]]
meta = frappe.get_meta(doctype)

View file

@ -475,28 +475,20 @@ class QueueBuilder:
if self._unsubscribed_user_emails is not None:
return self._unsubscribed_user_emails
all_ids = tuple(set(self.recipients + self.cc))
all_ids = list(set(self.recipients + self.cc))
unsubscribed = frappe.db.sql_list('''
SELECT
distinct email
from
`tabEmail Unsubscribe`
where
email in %(all_ids)s
and (
(
reference_doctype = %(reference_doctype)s
and reference_name = %(reference_name)s
)
or global_unsubscribe = 1
)
''', {
'all_ids': all_ids,
'reference_doctype': self.reference_doctype,
'reference_name': self.reference_name,
})
EmailUnsubscribe = frappe.qb.DocType("Email Unsubscribe")
unsubscribed = (frappe.qb.from_(EmailUnsubscribe)
.select(EmailUnsubscribe.email)
.where(EmailUnsubscribe.email.isin(all_ids) &
(
(
(EmailUnsubscribe.reference_doctype == self.reference_doctype) & (EmailUnsubscribe.reference_name == self.reference_name)
) | EmailUnsubscribe.global_unsubscribe == 1
)
).distinct()
).run(pluck=True)
self._unsubscribed_user_emails = unsubscribed or []
return self._unsubscribed_user_emails

View file

@ -27,11 +27,7 @@ from frappe.utils.html_utils import clean_email_html
# fix due to a python bug in poplib that limits it to 2048
poplib._MAXLINE = 20480
imaplib._MAXLINE = 20480
# fix due to a python bug in poplib that limits it to 2048
poplib._MAXLINE = 20480
imaplib._MAXLINE = 20480
class EmailSizeExceededError(frappe.ValidationError): pass

View file

@ -1,11 +1,13 @@
import requests
import json
import frappe
import base64
'''
FrappeClient is a library that helps you connect with other frappe systems
'''
import base64
import json
import requests
import frappe
class AuthError(Exception):
pass
@ -46,7 +48,7 @@ class FrappeClient(object):
def _login(self, username, password):
'''Login/start a sesion. Called internally on init'''
r = self.session.post(self.url, data={
r = self.session.post(self.url, params={
'cmd': 'login',
'usr': username,
'pwd': password
@ -289,14 +291,14 @@ class FrappeClient(object):
def get_api(self, method, params=None):
if params is None:
params = {}
res = self.session.get(self.url + "/api/method/" + method + "/",
res = self.session.get(f"{self.url}/api/method/{method}",
params=params, verify=self.verify, headers=self.headers)
return self.post_process(res)
def post_api(self, method, params=None):
if params is None:
params = {}
res = self.session.post(self.url + "/api/method/" + method + "/",
res = self.session.post(f"{self.url}/api/method/{method}",
params=params, verify=self.verify, headers=self.headers)
return self.post_process(res)

View file

@ -58,9 +58,9 @@ class LDAPSettings(Document):
import ssl
if self.require_trusted_certificate == 'Yes':
tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLSv1)
tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLS_CLIENT)
else:
tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1)
tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLS_CLIENT)
if self.local_private_key_file:
tls_configuration.private_key_file = self.local_private_key_file

View file

@ -296,7 +296,7 @@ class LDAP_TestCase():
if local_doc['require_trusted_certificate'] == 'Yes':
tls_validate = ssl.CERT_REQUIRED
tls_version = ssl.PROTOCOL_TLSv1
tls_version = ssl.PROTOCOL_TLS_CLIENT
tls_configuration = ldap3.Tls(validate=tls_validate, version=tls_version)
self.assertTrue(kwargs['auto_bind'] == ldap3.AUTO_BIND_TLS_BEFORE_BIND,
@ -304,7 +304,7 @@ class LDAP_TestCase():
else:
tls_validate = ssl.CERT_NONE
tls_version = ssl.PROTOCOL_TLSv1
tls_version = ssl.PROTOCOL_TLS_CLIENT
tls_configuration = ldap3.Tls(validate=tls_validate, version=tls_version)
self.assertTrue(kwargs['auto_bind'],

View file

@ -768,7 +768,9 @@ class BaseDocument(object):
else:
self_value = self.get_value(key)
# Postgres stores values as `datetime.time`, MariaDB as `timedelta`
if isinstance(self_value, datetime.timedelta) and isinstance(db_value, datetime.time):
db_value = datetime.timedelta(hours=db_value.hour, minutes=db_value.minute, seconds=db_value.second, microseconds=db_value.microsecond)
if self_value != db_value:
frappe.throw(_("Not allowed to change {0} after submission").format(df.label),
frappe.UpdateAfterSubmitError)
@ -1008,15 +1010,12 @@ def _filter(data, filters, limit=None):
_filters[f] = fval
for d in data:
add = True
for f, fval in _filters.items():
if not frappe.compare(getattr(d, f, None), fval[0], fval[1]):
add = False
break
if add:
else:
out.append(d)
if limit and (len(out)-1)==limit:
if limit and len(out) >= limit:
break
return out

View file

@ -130,6 +130,11 @@ class DatabaseQuery(object):
args.fields = 'distinct ' + args.fields
args.order_by = '' # TODO: recheck for alternative
# Postgres requires any field that appears in the select clause to also
# appear in the order by and group by clause
if frappe.db.db_type == 'postgres' and args.order_by and args.group_by:
args = self.prepare_select_args(args)
query = """select %(fields)s
from %(tables)s
%(conditions)s
@ -203,6 +208,19 @@ class DatabaseQuery(object):
return args
def prepare_select_args(self, args):
order_field = re.sub(r"\ order\ by\ |\ asc|\ ASC|\ desc|\ DESC", "", args.order_by)
if order_field not in args.fields:
extracted_column = order_column = order_field.replace("`", "")
if "." in extracted_column:
extracted_column = extracted_column.split(".")[1]
args.fields += f", MAX({extracted_column}) as `{order_column}`"
args.order_by = args.order_by.replace(order_field, f"`{order_column}`")
return args
def parse_args(self):
"""Convert fields and filters from strings to list, dicts"""
if isinstance(self.fields, str):

View file

@ -117,7 +117,8 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
doctype=doc.doctype, name=doc.name,
is_async=False if frappe.flags.in_test else True)
# clear cache for Document
doc.clear_cache()
# delete global search entry
delete_for_document(doc)
# delete tag link entry

View file

@ -396,6 +396,7 @@ class Document(BaseDocument):
"parenttype": self.doctype,
"parentfield": fieldname
})
def get_doc_before_save(self):
return getattr(self, '_doc_before_save', None)
@ -468,10 +469,12 @@ class Document(BaseDocument):
self._original_modified = self.modified
self.modified = now()
self.modified_by = frappe.session.user
if not self.creation:
self.creation = self.modified
# We'd probably want the creation and owner to be set via API
# or Data import at some point, that'd have to be handled here
if self.is_new():
self.owner = self.flags.owner or self.modified_by
self.creation = self.modified
self.owner = self.modified_by
for d in self.get_all_children():
d.modified = self.modified
@ -501,7 +504,6 @@ class Document(BaseDocument):
self._sanitize_content()
self._save_passwords()
self.validate_workflow()
self.validate_owner()
children = self.get_all_children()
for d in children:
@ -544,11 +546,6 @@ class Document(BaseDocument):
if not self._action == 'save':
set_workflow_state_on_action(self, workflow, self._action)
def validate_owner(self):
"""Validate if the owner of the Document has changed"""
if not self.is_new() and self.has_value_changed('owner'):
frappe.throw(_('Document owner cannot be changed'))
def validate_set_only_once(self):
"""Validate that fields are not changed if not in insert"""
set_only_once_fields = self.meta.get_set_only_once_fields()
@ -568,8 +565,12 @@ class Document(BaseDocument):
fail = value != original_value
if fail:
frappe.throw(_("Value cannot be changed for {0}").format(self.meta.get_label(field.fieldname)),
frappe.CannotChangeConstantError)
frappe.throw(
_("Value cannot be changed for {0}").format(
frappe.bold(self.meta.get_label(field.fieldname))
),
exc=frappe.CannotChangeConstantError
)
return False

View file

@ -67,6 +67,10 @@ class Meta(Document):
_metaclass = True
default_fields = list(default_fields)[1:]
special_doctypes = ("DocField", "DocPerm", "DocType", "Module Def", 'DocType Action', 'DocType Link', 'DocType State')
standard_set_once_fields = [
frappe._dict(fieldname="creation", fieldtype="Datetime"),
frappe._dict(fieldname="owner", fieldtype="Data"),
]
def __init__(self, doctype):
self._fields = {}
@ -154,6 +158,12 @@ class Meta(Document):
'''Return fields with `set_only_once` set'''
if not hasattr(self, "_set_only_once_fields"):
self._set_only_once_fields = self.get("fields", {"set_only_once": 1})
fieldnames = [d.fieldname for d in self._set_only_once_fields]
for df in self.standard_set_once_fields:
if df.fieldname not in fieldnames:
self._set_only_once_fields.append(df)
return self._set_only_once_fields
def get_table_fields(self):

View file

@ -110,6 +110,7 @@ def rename_doc(
if merge:
frappe.delete_doc(doctype, old)
new_doc.clear_cache()
frappe.clear_cache()
if rebuild_search:
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype)

View file

@ -182,6 +182,7 @@ frappe.patches.v13_0.queryreport_columns
execute:frappe.reload_doc('core', 'doctype', 'doctype')
frappe.patches.v13_0.jinja_hook
frappe.patches.v13_0.update_notification_channel_if_empty
frappe.patches.v13_0.set_first_day_of_the_week
frappe.patches.v14_0.drop_data_import_legacy
frappe.patches.v14_0.rename_cancelled_documents
frappe.patches.v14_0.copy_mail_data #08.03.21

View file

@ -0,0 +1,7 @@
import frappe
def execute():
frappe.reload_doctype("System Settings")
# setting first_day_of_the_week value as "Monday" to avoid breaking change
# because before the configuration was introduced, system used to consider "Monday" as start of the week
frappe.db.set_value("System Settings", "System Settings", "first_day_of_the_week", "Monday")

View file

@ -63,6 +63,7 @@ import "./frappe/utils/address_and_contact.js";
import "./frappe/utils/preview_email.js";
import "./frappe/utils/file_manager.js";
import "./frappe/utils/diffview";
import "./frappe/utils/datatable.js";
import "./frappe/upload.js";
import "./frappe/ui/tree.js";

View file

@ -142,6 +142,8 @@ frappe.data_import.ImportPreview = class ImportPreview {
columns: this.columns,
layout: this.columns.length < 10 ? 'fluid' : 'fixed',
cellHeight: 35,
language: frappe.boot.lang,
translations: frappe.utils.datatable.get_translations(),
serialNoColumn: false,
checkboxColumn: false,
noDataMessage: __('No Data'),

View file

@ -275,11 +275,7 @@ frappe.Application = class Application {
this.set_globals();
this.sync_pages();
frappe.router.setup();
moment.locale("en");
moment.user_utc_offset = moment().utcOffset();
if(frappe.boot.timezone_info) {
moment.tz.add(frappe.boot.timezone_info);
}
this.setup_moment();
if(frappe.boot.print_css) {
frappe.dom.set_style(frappe.boot.print_css, "print-style");
}
@ -628,6 +624,19 @@ frappe.Application = class Application {
}
});
}
setup_moment() {
moment.updateLocale('en', {
week: {
dow: frappe.datetime.get_first_day_of_the_week_index(),
}
});
moment.locale("en");
moment.user_utc_offset = moment().utcOffset();
if (frappe.boot.timezone_info) {
moment.tz.add(frappe.boot.timezone_info);
}
}
}
frappe.get_module = function(m, default_module) {

View file

@ -148,8 +148,9 @@ frappe.ui.form.Control = class BaseControl {
return this.doc[this.df.fieldname];
}
}
set_value(value) {
return this.validate_and_set_in_model(value);
set_value(value, force_set_value=false) {
return this.validate_and_set_in_model(value, null, force_set_value);
}
parse_validate_and_set_in_model(value, e) {
if(this.parse) {
@ -157,12 +158,11 @@ frappe.ui.form.Control = class BaseControl {
}
return this.validate_and_set_in_model(value, e);
}
validate_and_set_in_model(value, e) {
var me = this;
let force_value_set = (this.doc && this.doc.__run_link_triggers);
let is_value_same = (this.get_model_value() === value);
validate_and_set_in_model(value, e, force_set_value=false) {
const me = this;
const is_value_same = (this.get_model_value() === value);
if (this.inside_change_event || (!force_value_set && is_value_same)) {
if (this.inside_change_event || (is_value_same && !force_set_value)) {
return Promise.resolve();
}

View file

@ -10,14 +10,16 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
this.set_t_for_today();
}
set_formatted_input(value) {
if (value === "Today") {
value = this.get_now_date();
}
super.set_formatted_input(value);
if (this.timepicker_only) return;
if (!this.datepicker) return;
if (!value) {
this.datepicker.clear();
return;
} else if (value === "Today") {
value = this.get_now_date();
}
let should_refresh = this.last_value && this.last_value !== value;
@ -62,6 +64,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
dateFormat: date_format,
startDate: this.get_start_date(),
keyboardNav: false,
firstDay: frappe.datetime.get_first_day_of_the_week_index(),
onSelect: () => {
this.$input.trigger('change');
},
@ -77,7 +80,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
}
get_start_date() {
return new Date(this.get_now_date());
return this.get_now_date();
}
set_datepicker() {
@ -116,7 +119,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
this.datepicker.update('position', position);
}
get_now_date() {
return frappe.datetime.convert_to_system_tz(frappe.datetime.now_date(true));
return frappe.datetime.convert_to_system_tz(frappe.datetime.now_date(true), false).toDate();
}
set_t_for_today() {
var me = this;

View file

@ -983,7 +983,7 @@ frappe.ui.form.Form = class FrappeForm {
$.each(this.fields_dict, function(fieldname, field) {
if (field.df.fieldtype=="Link" && this.doc[fieldname]) {
// triggers add fetch, sets value in model and runs triggers
field.set_value(this.doc[fieldname]);
field.set_value(this.doc[fieldname], true);
}
});

View file

@ -196,7 +196,7 @@ export default class GridRow {
// REDESIGN-TODO: Make translation contextual, this No is Number
var txt = (this.doc ? this.doc.idx : __("No."));
this.row_index = $(
`<div class="row-index sortable-handle col col-xs-1">
`<div class="row-index sortable-handle col">
${this.row_check_html}
<span class="hidden-xs">${txt}</span></div>`)
.appendTo(this.row)

View file

@ -0,0 +1,22 @@
frappe.provide("frappe.utils.datatable");
frappe.utils.datatable.get_translations = function () {
let translations = {};
translations[frappe.boot.lang] = {
"Sort Ascending": __("Sort Ascending"),
"Sort Descending": __("Sort Descending"),
"Reset sorting": __("Reset sorting"),
"Remove column": __("Remove column"),
"No Data": __("No Data"),
"{count} cells copied": {
"1": __("{count} cell copied"),
"default": __("{count} cells copied")
},
"{count} rows selected": {
"1": __("{count} row selected"),
"default": __("{count} rows selected")
}
};
return translations;
};

View file

@ -254,6 +254,11 @@ $.extend(frappe.datetime, {
], true).isValid();
},
get_first_day_of_the_week_index() {
const first_day_of_the_week = frappe.sys_defaults.first_day_of_the_week || "Sunday";
return moment.weekdays().indexOf(first_day_of_the_week);
}
});
// Proxy for dateutil and get_today

View file

@ -105,7 +105,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
this.toggle_nothing_to_show(true);
return;
}
let route_options = {};
route_options = Object.assign(route_options, frappe.route_options);
@ -849,6 +849,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
columns: columns,
data: data,
inlineFilters: true,
language: frappe.boot.lang,
translations: frappe.utils.datatable.get_translations(),
treeView: this.tree_report,
layout: 'fixed',
cellHeight: 33,

View file

@ -284,6 +284,8 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
columns: this.columns,
data: this.get_data(values),
getEditor: this.get_editing_object.bind(this),
language: frappe.boot.lang,
translations: frappe.utils.datatable.get_translations(),
checkboxColumn: true,
inlineFilters: true,
cellHeight: 35,

View file

@ -9,6 +9,7 @@ export default class WebForm extends frappe.ui.FieldGroup {
frappe.web_form = this;
frappe.web_form.events = {};
Object.assign(frappe.web_form.events, EventEmitterMixin);
this.current_section = 0;
}
prepare(web_form_doc, doc) {
@ -19,12 +20,16 @@ export default class WebForm extends frappe.ui.FieldGroup {
make() {
super.make();
this.set_sections();
this.set_field_values();
this.setup_listeners();
if (this.introduction_text) this.set_form_description(this.introduction_text);
if (this.allow_print && !this.is_new) this.setup_print_button();
if (this.allow_delete && !this.is_new) this.setup_delete_button();
if (this.is_new) this.setup_cancel_button();
this.setup_primary_action();
this.setup_previous_next_button();
this.toggle_section();
$(".link-btn").remove();
// webform client script
@ -40,6 +45,88 @@ export default class WebForm extends frappe.ui.FieldGroup {
};
}
setup_listeners() {
// Event listener for triggering Save/Next button for Multi Step Forms
// Do not use `on` event here since that can be used by user which will render this function useless
// setTimeout has 200ms delay so that all the base_control triggers for the fields have been run
let me = this;
if (!me.is_multi_step_form) {
return;
}
for (let field of $(".input-with-feedback")) {
$(field).change((e) => {
setTimeout(() => {
e.stopPropagation();
me.toggle_buttons();
}, 200);
});
}
}
set_sections() {
if (this.sections.length) return;
this.sections = $(`.form-section`);
}
setup_previous_next_button() {
let me = this;
if (!me.is_multi_step_form) {
return;
}
$('.web-form-footer').after(`
<div id="form-step-footer" class="pull-right">
<button class="btn btn-primary btn-previous btn-sm ml-2">${__("Previous")}</button>
<button class="btn btn-primary btn-next btn-sm ml-2">${__("Next")}</button>
</div>
`);
$('.btn-previous').on('click', function () {
let is_validated = me.validate_section();
if (!is_validated) return;
/**
The eslint utility cannot figure out if this is an infinite loop in backwards and
throws an error. Disabling for-direction just for this section.
for-direction doesnt throw an error if the values are hardcoded in the
reverse for-loop, but in this case its a dynamic loop.
https://eslint.org/docs/rules/for-direction
*/
/* eslint-disable for-direction */
for (let idx = me.current_section; idx < me.sections.length; idx--) {
let is_empty = me.is_previous_section_empty(idx);
me.current_section = me.current_section > 0 ? me.current_section - 1 : me.current_section;
if (!is_empty) {
break;
}
}
/* eslint-enable for-direction */
me.toggle_section();
});
$('.btn-next').on('click', function () {
let is_validated = me.validate_section();
if (!is_validated) return;
for (let idx = me.current_section; idx < me.sections.length; idx++) {
let is_empty = me.is_next_section_empty(idx);
me.current_section = me.current_section < me.sections.length ? me.current_section + 1 : me.current_section;
if (!is_empty) {
break;
}
}
me.toggle_section();
});
}
set_field_values() {
if (this.doc.name) this.set_values(this.doc);
else return;
@ -104,6 +191,113 @@ export default class WebForm extends frappe.ui.FieldGroup {
);
}
validate_section() {
if (this.allow_incomplete) return true;
let fields = $(`.form-section:eq(${this.current_section}) .form-control`);
let errors = [];
let invalid_values = [];
for (let field of fields) {
let fieldname = $(field).attr("data-fieldname");
if (!fieldname) continue;
field = this.fields_dict[fieldname];
if (field.get_value) {
let value = field.get_value();
if (field.df.reqd && is_null(typeof value === 'string' ? strip_html(value) : value)) errors.push(__(field.df.label));
if (field.df.reqd && field.df.fieldtype === 'Text Editor' && is_null(strip_html(cstr(value)))) errors.push(__(field.df.label));
if (field.df.invalid) invalid_values.push(__(field.df.label));
}
}
let message = '';
if (invalid_values.length) {
message += __('Invalid values for fields:') + '<br><br><ul><li>' + invalid_values.join('<li>') + '</ul>';
}
if (errors.length) {
message += __('Mandatory fields required:') + '<br><br><ul><li>' + errors.join('<li>') + '</ul>';
}
if (invalid_values.length || errors.length) {
frappe.msgprint({
title: __('Error'),
message: message,
indicator: 'orange'
});
}
return !(errors.length || invalid_values.length);
}
toggle_section() {
if (!this.is_multi_step_form) return;
this.toggle_previous_button();
this.hide_sections();
this.show_section();
this.toggle_buttons();
}
toggle_buttons() {
for (let idx = this.current_section; idx < this.sections.length; idx++) {
if (this.is_next_section_empty(idx)) {
this.show_save_and_hide_next_button();
} else {
this.show_next_and_hide_save_button();
break;
}
}
}
is_next_section_empty(section) {
if (section + 1 > this.sections.length) return true;
let _section = $(`.form-section:eq(${section + 1})`);
let visible_controls = _section.find(".frappe-control:not(.hide-control)");
return !visible_controls.length ? true : false;
}
is_previous_section_empty(section) {
if (section - 1 > this.sections.length) return true;
let _section = $(`.form-section:eq(${section - 1})`);
let visible_controls = _section.find(".frappe-control:not(.hide-control)");
return !visible_controls.length ? true : false;
}
show_save_and_hide_next_button() {
$('.btn-next').hide();
$('.web-form-footer').show();
}
show_next_and_hide_save_button() {
$('.btn-next').show();
$('.web-form-footer').hide();
}
toggle_previous_button() {
this.current_section == 0 ? $('.btn-previous').hide() : $('.btn-previous').show();
}
show_section() {
$(`.form-section:eq(${this.current_section})`).show();
}
hide_sections() {
for (let idx=0; idx < this.sections.length; idx++) {
if (idx !== this.current_section) {
$(`.form-section:eq(${idx})`).hide();
}
}
}
save() {
let is_new = this.is_new;
if (this.validate && !this.validate()) {

View file

@ -39,6 +39,7 @@
padding: var(--padding-sm);
color: var(--text-color);
border-radius: var(--border-radius);
white-space: unset;
@extend .ellipsis;
&:not(:last-child) {
margin-bottom: var(--margin-xs);

View file

@ -54,7 +54,7 @@
.form-section.card-section,
.form-dashboard-section {
border-bottom: 1px solid var(--gray-200);
border-bottom: 1px solid var(--border-color);
padding: var(--padding-xs);
}
@ -316,12 +316,12 @@
.form-tabs-list {
padding-left: var(--padding-xs);
border-bottom: 1px solid var(--gray-200);
border-bottom: 1px solid var(--border-color);
.form-tabs {
.nav-item {
.nav-link {
color: var(--gray-700);
color: var(--text-muted);
padding: var(--padding-md) 0;
margin: 0 var(--margin-md);

View file

@ -335,7 +335,10 @@ def make_test_records_for_doctype(doctype, verbose=0, force=False):
frappe.local.test_objects[doctype] += test_module._make_test_records(verbose)
elif hasattr(test_module, "test_records"):
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose, force)
if doctype in frappe.local.test_objects:
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose, force)
else:
frappe.local.test_objects[doctype] = make_test_objects(doctype, test_module.test_records, verbose, force)
else:
test_records = frappe.get_test_records(doctype)

View file

@ -13,7 +13,7 @@ class TestAssign(unittest.TestCase):
added = assign(todo, "test@example.com")
self.assertTrue("test@example.com" in [d.allocated_to for d in added])
self.assertTrue("test@example.com" in [d.owner for d in added])
removed = frappe.desk.form.assign_to.remove(todo.doctype, todo.name, "test@example.com")

View file

@ -1,6 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe, unittest
import frappe
import datetime
import unittest
from frappe.model.db_query import DatabaseQuery
from frappe.desk.reportview import get_filters_cond
@ -380,6 +382,22 @@ class TestReportview(unittest.TestCase):
owners = DatabaseQuery("DocType").execute(filters={"name": "DocType"}, pluck="owner")
self.assertEqual(owners, ["Administrator"])
def test_prepare_select_args(self):
# frappe.get_all inserts modified field into order_by clause
# test to make sure this is inserted into select field when postgres
doctypes = frappe.get_all("DocType",
filters={"docstatus": 0, "document_type": ("!=", "")},
group_by="document_type",
fields=["document_type", "sum(is_submittable) as is_submittable"],
limit=1,
as_list=True,
)
if frappe.conf.db_type == "mariadb":
self.assertTrue(len(doctypes[0]) == 2)
else:
self.assertTrue(len(doctypes[0]) == 3)
self.assertTrue(isinstance(doctypes[0][2], datetime.datetime))
def test_column_comparison(self):
"""Test DatabaseQuery.execute to test column comparison
"""

View file

@ -253,21 +253,7 @@ class TestDocument(unittest.TestCase):
})
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')
def test_owner_changed(self):
frappe.delete_doc_if_exists("User", "hello@example.com")
frappe.set_user("Administrator")
d = frappe.get_doc({
"doctype": "User",
"email": "hello@example.com",
"first_name": "John"
})
d.insert()
self.assertEqual(frappe.db.get_value("User", d.owner), d.owner)
d.set("owner", "johndoe@gmail.com")
self.assertRaises(frappe.ValidationError, d.save)
d.reload()
d.save()
self.assertEqual(frappe.db.get_value("User", d.owner), d.owner)
def test_limit_for_get(self):
doc = frappe.get_doc("DocType", "DocType")
# assuming DocType has more that 3 Data fields
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)

View file

@ -10,8 +10,9 @@ import requests
import base64
class TestFrappeClient(unittest.TestCase):
PASSWORD = "admin"
def test_insert_many(self):
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
frappe.db.delete("Note", {"title": ("in", ('Sing','a','song','of','sixpence'))})
frappe.db.commit()
@ -30,7 +31,7 @@ class TestFrappeClient(unittest.TestCase):
self.assertTrue(frappe.db.get_value('Note', {'title': 'sixpence'}))
def test_create_doc(self):
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
frappe.db.delete("Note", {"title": "test_create"})
frappe.db.commit()
@ -39,13 +40,13 @@ class TestFrappeClient(unittest.TestCase):
self.assertTrue(frappe.db.get_value('Note', {'title': 'test_create'}))
def test_list_docs(self):
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
doc_list = server.get_list("Note")
self.assertTrue(len(doc_list))
def test_get_doc(self):
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
frappe.db.delete("Note", {"title": "get_this"})
frappe.db.commit()
@ -56,7 +57,7 @@ class TestFrappeClient(unittest.TestCase):
self.assertTrue(doc)
def test_get_value(self):
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
frappe.db.delete("Note", {"title": "get_value"})
frappe.db.commit()
@ -74,14 +75,14 @@ class TestFrappeClient(unittest.TestCase):
self.assertRaises(FrappeException, server.get_value, "Note", "(select (password) from(__Auth) order by name desc limit 1)", {"title": "get_value"})
def test_get_single(self):
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
server.set_value('Website Settings', 'Website Settings', 'title_prefix', 'test-prefix')
self.assertEqual(server.get_value('Website Settings', 'title_prefix', 'Website Settings').get('title_prefix'), 'test-prefix')
self.assertEqual(server.get_value('Website Settings', 'title_prefix').get('title_prefix'), 'test-prefix')
frappe.db.set_value('Website Settings', None, 'title_prefix', '')
def test_update_doc(self):
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
frappe.db.delete("Note", {"title": ("in", ("Sing", "sing"))})
frappe.db.commit()
@ -93,7 +94,7 @@ class TestFrappeClient(unittest.TestCase):
self.assertTrue(doc["title"] == changed_title)
def test_update_child_doc(self):
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
frappe.db.delete("Contact", {"first_name": "George", "last_name": "Steevens"})
frappe.db.delete("Contact", {"first_name": "William", "last_name": "Shakespeare"})
frappe.db.delete("Communication", {"reference_doctype": "Event"})
@ -130,7 +131,7 @@ class TestFrappeClient(unittest.TestCase):
self.assertTrue(frappe.db.exists("Communication Link", {"link_name": "William Shakespeare"}))
def test_delete_doc(self):
server = FrappeClient(get_url(), "Administrator", "admin", verify=False)
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
frappe.db.delete("Note", {"title": "delete"})
frappe.db.commit()

View file

@ -12,6 +12,7 @@ from frappe.core.page.permission_manager.permission_manager import update, reset
from frappe.test_runner import make_test_records_for_doctype
from frappe.core.doctype.user_permission.user_permission import clear_user_permissions
from frappe.desk.form.load import getdoc
from frappe.utils.data import now_datetime
test_dependencies = ['Blogger', 'Blog Post', "User", "Contact", "Salutation"]
@ -197,6 +198,32 @@ class TestPermissions(unittest.TestCase):
doc = frappe.get_doc("Blog Post", "-test-blog-post")
self.assertTrue(doc.has_permission("read"))
def test_set_standard_fields_manually(self):
# check that creation and owner cannot be set manually
from datetime import timedelta
fake_creation = now_datetime() + timedelta(days=-7)
fake_owner = frappe.db.get_value("User", {"name": ("!=", frappe.session.user)})
d = frappe.new_doc("ToDo")
d.description = "ToDo created via test_set_standard_fields_manually"
d.creation = fake_creation
d.owner = fake_owner
d.save()
self.assertNotEqual(d.creation, fake_creation)
self.assertNotEqual(d.owner, fake_owner)
def test_dont_change_standard_constants(self):
# check that Document.creation cannot be changed
user = frappe.get_doc("User", frappe.session.user)
user.creation = now_datetime()
self.assertRaises(frappe.CannotChangeConstantError, user.save)
# check that Document.owner cannot be changed
user.reload()
user.owner = frappe.db.get_value("User", {"name": ("!=", user.name)})
self.assertRaises(frappe.CannotChangeConstantError, user.save)
def test_set_only_once(self):
blog_post = frappe.get_meta("Blog Post")
doc = frappe.get_doc("Blog Post", "-test-blog-post-1")

View file

@ -15,6 +15,8 @@ import io
from mimetypes import guess_type
from datetime import datetime, timedelta, date
from unittest.mock import patch
class TestFilters(unittest.TestCase):
def test_simple_dict(self):
self.assertTrue(evaluate_filters({'doctype': 'User', 'status': 'Open'}, {'status': 'Open'}))
@ -306,3 +308,24 @@ class TestDiffUtils(unittest.TestCase):
diff = get_version_diff(old_version, latest_version)
self.assertIn('-2;', diff)
self.assertIn('+42;', diff)
class TestDateUtils(unittest.TestCase):
def test_first_day_of_week(self):
# Monday as start of the week
with patch.object(frappe.utils.data, "get_first_day_of_the_week", return_value="Monday"):
self.assertEqual(frappe.utils.get_first_day_of_week("2020-12-25"),
frappe.utils.getdate("2020-12-21"))
self.assertEqual(frappe.utils.get_first_day_of_week("2020-12-20"),
frappe.utils.getdate("2020-12-14"))
# Sunday as start of the week
self.assertEqual(frappe.utils.get_first_day_of_week("2020-12-25"),
frappe.utils.getdate("2020-12-20"))
self.assertEqual(frappe.utils.get_first_day_of_week("2020-12-21"),
frappe.utils.getdate("2020-12-20"))
def test_last_day_of_week(self):
self.assertEqual(frappe.utils.get_last_day_of_week("2020-12-24"),
frappe.utils.getdate("2020-12-26"))
self.assertEqual(frappe.utils.get_last_day_of_week("2020-12-28"),
frappe.utils.getdate("2021-01-02"))

View file

@ -197,6 +197,7 @@ class TestWebsite(unittest.TestCase):
frappe.cache().delete_key('app_hooks')
def test_printview_page(self):
frappe.db.value_cache[('DocType', 'Language', 'name')] = (('Language',),)
content = get_response_content('/Language/ru')
self.assertIn('<div class="print-format">', content)
self.assertIn('<div>Language</div>', content)

View file

@ -244,3 +244,14 @@ def create_topic_and_reply(web_page):
})
reply.save()
@frappe.whitelist()
def update_webform_to_multistep():
doc = frappe.get_doc("Web Form", "edit-profile")
_doc = frappe.copy_doc(doc)
_doc.is_multi_step_form = 1
_doc.title = "update-profile-duplicate"
_doc.route = "update-profile-duplicate"
_doc.is_standard = False
_doc.save()

View file

@ -4700,3 +4700,7 @@ Value cannot be negative for {0}: {1},Der Wert kann für {0} nicht negativ sein:
Negative Value,Negativer Wert,
Authentication failed while receiving emails from Email Account: {0}.,Die Authentifizierung ist beim Empfang von E-Mails vom E-Mail-Konto fehlgeschlagen: {0}.,
Message from server: {0},Nachricht vom Server: {0},
Reset sorting,Sortierung zurücksetzen,
Sort Ascending,Aufsteigend sortieren,
Sort Descending,Absteigend sortieren,
Remove column,Spalte entfernen,

1 A4 A4
4700 Negative Value Negativer Wert
4701 Authentication failed while receiving emails from Email Account: {0}. Die Authentifizierung ist beim Empfang von E-Mails vom E-Mail-Konto fehlgeschlagen: {0}.
4702 Message from server: {0} Nachricht vom Server: {0}
4703 Reset sorting Sortierung zurücksetzen
4704 Sort Ascending Aufsteigend sortieren
4705 Sort Descending Absteigend sortieren
4706 Remove column Spalte entfernen

View file

@ -11,11 +11,26 @@ from code import compile_command
from urllib.parse import quote, urljoin
from frappe.desk.utils import slug
from click import secho
from enum import Enum
DATE_FORMAT = "%Y-%m-%d"
TIME_FORMAT = "%H:%M:%S.%f"
DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT
class Weekday(Enum):
Sunday = 0
Monday = 1
Tuesday = 2
Wednesday = 3
Thursday = 4
Friday = 5
Saturday = 6
def get_first_day_of_the_week():
return frappe.get_system_settings('first_day_of_the_week') or "Sunday"
def get_start_of_week_index():
return Weekday[get_first_day_of_the_week()].value
def is_invalid_date_string(date_string):
# dateutil parser does not agree with dates like "0001-01-01" or "0000-00-00"
@ -98,6 +113,9 @@ def get_timedelta(time: Optional[str] = None) -> Optional[datetime.timedelta]:
def to_timedelta(time_str):
from dateutil import parser
if isinstance(time_str, datetime.time):
time_str = str(time_str)
if isinstance(time_str, str):
t = parser.parse(time_str)
return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond)
@ -246,9 +264,22 @@ def get_quarter_start(dt, as_str=False):
def get_first_day_of_week(dt, as_str=False):
dt = getdate(dt)
date = dt - datetime.timedelta(days=dt.weekday())
date = dt - datetime.timedelta(days=get_week_start_offset_days(dt))
return date.strftime(DATE_FORMAT) if as_str else date
def get_week_start_offset_days(dt):
current_day_index = get_normalized_weekday_index(dt)
start_of_week_index = get_start_of_week_index()
if current_day_index >= start_of_week_index:
return current_day_index - start_of_week_index
else:
return 7 - (start_of_week_index - current_day_index)
def get_normalized_weekday_index(dt):
# starts Sunday with 0
return (dt.weekday() + 1) % 7
def get_year_start(dt, as_str=False):
dt = getdate(dt)
date = datetime.date(dt.year, 1, 1)

View file

@ -5,11 +5,11 @@ import getpass
from frappe.utils.password import update_password
def before_install():
frappe.reload_doc("core", "doctype", "doctype_state")
frappe.reload_doc("core", "doctype", "docfield")
frappe.reload_doc("core", "doctype", "docperm")
frappe.reload_doc("core", "doctype", "doctype_action")
frappe.reload_doc("core", "doctype", "doctype_link")
frappe.reload_doc("core", "doctype", "doctype_state")
frappe.reload_doc("desk", "doctype", "form_tour_step")
frappe.reload_doc("desk", "doctype", "form_tour")
frappe.reload_doc("core", "doctype", "doctype")

View file

@ -35,9 +35,13 @@ def get_random(doctype, filters=None, doc=False):
condition = " where " + " and ".join(condition)
else:
condition = ""
out = frappe.db.sql("""select name from `tab%s` %s
order by RAND() limit 0,1""" % (doctype, condition))
out = frappe.db.multisql({
'mariadb': """select name from `tab%s` %s
order by RAND() limit 1 offset 0""" % (doctype, condition),
'postgres': """select name from `tab%s` %s
order by RANDOM() limit 1 offset 0""" % (doctype, condition)
})
out = out and out[0][0] or None

View file

@ -1,4 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
# Tree (Hierarchical) Nested Set Model (nsm)
@ -109,7 +109,6 @@ def update_move_node(doc, parent_field):
new_parent = frappe.db.sql("""select lft, rgt from `tab%s`
where name = %s for update""" % (doc.doctype, '%s'), parent, as_dict=1)[0]
# set parent lft, rgt
frappe.db.sql("""update `tab{0}` set rgt = rgt + %s
where name = %s""".format(doc.doctype), (diff, parent))
@ -134,6 +133,7 @@ def update_move_node(doc, parent_field):
frappe.db.sql("""update `tab{0}` set lft = -lft + %s, rgt = -rgt + %s
where lft < 0""".format(doc.doctype), (new_diff, new_diff))
@frappe.whitelist()
def rebuild_tree(doctype, parent_field):
"""
@ -153,7 +153,6 @@ def rebuild_tree(doctype, parent_field):
right = 1
table = DocType(doctype)
column = getattr(table, parent_field)
result = (
frappe.qb.from_(table)
.where(

View file

@ -125,7 +125,7 @@ def json_handler(obj):
# serialize date
import collections.abc
if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)):
if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime, datetime.time)):
return str(obj)
elif isinstance(obj, decimal.Decimal):

View file

@ -52,5 +52,39 @@
{% endblock %}
{% block script %}
<script>{% include "templates/includes/list/list.js" %}</script>
<script>
frappe.ready(() => {
let result_wrapper = $(".website-list .result");
let next_start = {{ next_start or 0 }};
$(".website-list .btn-more").on("click", function() {
let $btn = $(this);
let args = $.extend(frappe.utils.get_query_params(), {
doctype: "Blog Post",
category: "{{ frappe.form_dict.category or '' }}",
limit_start: next_start,
pathname: location.pathname,
});
$btn.prop("disabled", true);
frappe.call('frappe.www.list.get', args)
.then(r => {
var data = r.message;
next_start = data.next_start;
$.each(data.result, function(i, d) {
$(d).appendTo(result_wrapper);
});
toggle_more(data.show_more);
})
.always(() => {
$btn.prop("disabled", false);
});
});
function toggle_more(show) {
if (!show) {
$(".website-list .more-block").addClass("hide");
}
}
});
</script>
{% endblock %}

View file

@ -58,15 +58,18 @@ class TestBlogPost(unittest.TestCase):
category_page_link = list(soup.find_all('a', href=re.compile(blog.blog_category)))[0]
category_page_url = category_page_link["href"]
cached_value = frappe.db.value_cache[('DocType', 'Blog Post', 'name')]
frappe.db.value_cache[('DocType', 'Blog Post', 'name')] = (('Blog Post',),)
# Visit the category page (by following the link found in above stage)
set_request(path=category_page_url)
category_page_response = get_response()
category_page_html = frappe.safe_decode(category_page_response.get_data())
# Category page should contain the blog post title
self.assertIn(blog.title, category_page_html)
# Cleanup
frappe.db.value_cache[('DocType', 'Blog Post', 'name')] = cached_value
frappe.delete_doc("Blog Post", blog.name)
frappe.delete_doc("Blog Category", blog.blog_category)

View file

@ -50,11 +50,10 @@ class TestPersonalDataDeletionRequest(unittest.TestCase):
def test_unverified_record_removal(self):
date_time_obj = datetime.strptime(
self.delete_request.creation, "%Y-%m-%d %H:%M:%S.%f"
)
date_time_obj += timedelta(days=-7)
self.delete_request.creation = date_time_obj
self.status = "Pending Verification"
self.delete_request.save()
) + timedelta(days=-7)
self.delete_request.db_set("creation", date_time_obj)
self.delete_request.db_set("status", "Pending Verification")
remove_unverified_record()
self.assertFalse(
frappe.db.exists("Personal Data Deletion Request", self.delete_request.name)

View file

@ -78,6 +78,10 @@ frappe.boot = {
sysdefaults: {
float_precision: parseInt("{{ frappe.get_system_settings('float_precision') or 3 }}"),
date_format: "{{ frappe.get_system_settings('date_format') or 'yyyy-mm-dd' }}",
},
time_zone: {
system: "{{ frappe.utils.get_time_zone() }}",
user: "{{ frappe.db.get_value('User', frappe.session.user, 'time_zone') or frappe.utils.get_time_zone() }}"
}
};
// for backward compatibility of some libs

View file

@ -11,6 +11,7 @@
"module",
"column_break_4",
"is_standard",
"is_multi_step_form",
"published",
"login_required",
"route_to_success_link",
@ -355,13 +356,19 @@
"fieldname": "apply_document_permissions",
"fieldtype": "Check",
"label": "Apply Document Permissions"
},
{
"default": "0",
"fieldname": "is_multi_step_form",
"fieldtype": "Check",
"label": "Is Multi Step Form"
}
],
"has_web_view": 1,
"icon": "icon-edit",
"is_published_field": "published",
"links": [],
"modified": "2020-08-07 13:12:03.945686",
"modified": "2021-11-15 14:12:44.624573",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form",

View file

@ -216,8 +216,8 @@ def get_context(context):
"amount": amount,
"title": title,
"description": title,
"reference_doctype": "Web Form",
"reference_docname": self.name,
"reference_doctype": doc.doctype,
"reference_docname": doc.name,
"payer_email": frappe.session.user,
"payer_name": frappe.utils.get_fullname(frappe.session.user),
"order_id": doc.name,

View file

@ -36,7 +36,7 @@
"express": "^4.17.1",
"fast-deep-equal": "^2.0.1",
"frappe-charts": "^2.0.0-rc13",
"frappe-datatable": "^1.15.4",
"frappe-datatable": "^1.16.0",
"frappe-gantt": "^0.5.0",
"fuse.js": "^3.4.6",
"highlight.js": "^10.4.1",

View file

@ -57,5 +57,5 @@ setup(
{
'clean': CleanCommand
},
python_requires='>=3.7'
python_requires='>=3.8'
)

View file

@ -554,9 +554,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001196, caniuse-lite@^1.0.30001219:
version "1.0.30001272"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001272.tgz"
integrity sha512-DV1j9Oot5dydyH1v28g25KoVm7l8MTxazwuiH3utWiAS6iL/9Nh//TGwqFEeqqN8nnWYQ8HHhUq+o4QPt9kvYw==
version "1.0.30001296"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001296.tgz"
integrity sha512-WfrtPEoNSoeATDlf4y3QvkwiELl9GyPLISV5GejTbbQRtQx4LhsXmc9IQ6XCL2d7UxCyEzToEZNMeqR79OUw8Q==
caseless@~0.12.0:
version "0.12.0"
@ -1601,10 +1601,10 @@ frappe-charts@^2.0.0-rc13:
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc13.tgz#fdb251d7ae311c41e38f90a3ae108070ec6b9072"
integrity sha512-Bv7IfllIrjRbKWHn5b769dOSenqdBixAr6m5kurf8ZUOJSLOgK4HOXItJ7BA8n9PvviH9/k5DaloisjLM2Bm1w==
frappe-datatable@^1.15.4:
version "1.15.4"
resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.15.4.tgz#dc2e5e5d8a0a7cb8ee658f2d39966af1d4405401"
integrity sha512-eW3upPvverm1GNBL4+IcPDvjm5xbJc5ZXW8TYEUZt/QQ2W75K/T6736pSzi9D6mX9sn3BtZ7Ige7MS45SGrgzQ==
frappe-datatable@^1.16.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.16.0.tgz#822dbcaaf48e5171f47ce2909da88e9a31bb2cbc"
integrity sha512-FkeHcaxxz4+BQLhwiegk94602PlnWNG4LiBPehgKdEL+OpJcwl8oNKWb/wPNw9lgW25u0LaaQwq/11sw7mnEbA==
dependencies:
hyperlist "^1.0.0-beta"
lodash "^4.17.5"