ソースを参照

Merge branch 'develop' into camera_upload

version-14
Raffael Meyer 3年前
committed by GitHub
コミット
ead843cf28
この署名に対応する既知のキーがデータベースに存在しません GPGキーID: 4AEE18F83AFDEB23
26個のファイルの変更197行の追加97行の削除
  1. +5
    -5
      frappe/__init__.py
  2. +2
    -2
      frappe/core/doctype/communication/test_communication.py
  3. +3
    -3
      frappe/core/doctype/file/test_file.py
  4. +2
    -2
      frappe/core/doctype/user/user.py
  5. +54
    -34
      frappe/database/database.py
  6. +2
    -2
      frappe/email/doctype/auto_email_report/auto_email_report.py
  7. +1
    -1
      frappe/email/doctype/notification/test_notification.py
  8. +7
    -10
      frappe/handler.py
  9. +1
    -1
      frappe/model/base_document.py
  10. +1
    -1
      frappe/permissions.py
  11. +4
    -4
      frappe/public/js/frappe/form/controls/attach.js
  12. +4
    -1
      frappe/public/js/frappe/form/footer/form_timeline.js
  13. +1
    -1
      frappe/public/js/frappe/list/list_settings.js
  14. +9
    -5
      frappe/public/js/frappe/ui/page.js
  15. +1
    -1
      frappe/public/js/frappe/utils/utils.js
  16. +1
    -1
      frappe/public/js/frappe/views/reports/report_view.js
  17. +36
    -6
      frappe/public/js/frappe/web_form/web_form_list.js
  18. +1
    -0
      frappe/public/scss/desk/page.scss
  19. +13
    -0
      frappe/public/scss/website/index.scss
  20. +3
    -3
      frappe/tests/test_base_document.py
  21. +38
    -3
      frappe/tests/test_db.py
  22. +3
    -3
      frappe/tests/test_document.py
  23. +2
    -2
      frappe/tests/test_search.py
  24. +0
    -2
      frappe/translate.py
  25. +3
    -3
      frappe/utils/backups.py
  26. +0
    -1
      frappe/utils/nestedset.py

+ 5
- 5
frappe/__init__.py ファイルの表示

@@ -978,8 +978,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa


def delete_doc_if_exists(doctype, name, force=0): def delete_doc_if_exists(doctype, name, force=0):
"""Delete document if exists.""" """Delete document if exists."""
if db.exists(doctype, name):
delete_doc(doctype, name, force=force)
delete_doc(doctype, name, force=force, ignore_missing=True)


def reload_doctype(doctype, force=False, reset_permissions=False): def reload_doctype(doctype, force=False, reset_permissions=False):
"""Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files.""" """Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files."""
@@ -1252,9 +1251,10 @@ def get_newargs(fn, kwargs):
if hasattr(fn, 'fnargs'): if hasattr(fn, 'fnargs'):
fnargs = fn.fnargs fnargs = fn.fnargs
else: else:
fnargs = inspect.getfullargspec(fn).args
fnargs.extend(inspect.getfullargspec(fn).kwonlyargs)
varkw = inspect.getfullargspec(fn).varkw
fullargspec = inspect.getfullargspec(fn)
fnargs = fullargspec.args
fnargs.extend(fullargspec.kwonlyargs)
varkw = fullargspec.varkw


newargs = {} newargs = {}
for a in kwargs: for a in kwargs:


+ 2
- 2
frappe/core/doctype/communication/test_communication.py ファイルの表示

@@ -4,8 +4,8 @@ import unittest
from urllib.parse import quote from urllib.parse import quote


import frappe import frappe
from frappe.email.doctype.email_queue.email_queue import EmailQueue
from frappe.core.doctype.communication.communication import get_emails from frappe.core.doctype.communication.communication import get_emails
from frappe.email.doctype.email_queue.email_queue import EmailQueue


test_records = frappe.get_test_records('Communication') test_records = frappe.get_test_records('Communication')


@@ -202,7 +202,7 @@ class TestCommunication(unittest.TestCase):


self.assertIn(("Note", note.name), doc_links) self.assertIn(("Note", note.name), doc_links)


def parse_emails(self):
def test_parse_emails(self):
emails = get_emails( emails = get_emails(
[ [
'comm_recipient+DocType+DocName@example.com', 'comm_recipient+DocType+DocName@example.com',


+ 3
- 3
frappe/core/doctype/file/test_file.py ファイルの表示

@@ -382,7 +382,7 @@ class TestFile(unittest.TestCase):
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)


test_file.make_thumbnail() test_file.make_thumbnail()
self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg')
self.assertEqual(test_file.thumbnail_url, '/files/image_small.jpg')


# test web image without extension # test web image without extension
test_file = frappe.get_doc({ test_file = frappe.get_doc({
@@ -399,7 +399,7 @@ class TestFile(unittest.TestCase):
test_file.reload() test_file.reload()
test_file.file_url = "/files/image_small.jpg" test_file.file_url = "/files/image_small.jpg"
test_file.make_thumbnail(suffix="xs", crop=True) test_file.make_thumbnail(suffix="xs", crop=True)
self.assertEquals(test_file.thumbnail_url, '/files/image_small_xs.jpg')
self.assertEqual(test_file.thumbnail_url, '/files/image_small_xs.jpg')


frappe.clear_messages() frappe.clear_messages()
test_file.db_set('thumbnail_url', None) test_file.db_set('thumbnail_url', None)
@@ -407,7 +407,7 @@ class TestFile(unittest.TestCase):
test_file.file_url = frappe.utils.get_url('unknown.jpg') test_file.file_url = frappe.utils.get_url('unknown.jpg')
test_file.make_thumbnail(suffix="xs") test_file.make_thumbnail(suffix="xs")
self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found") self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found")
self.assertEquals(test_file.thumbnail_url, None)
self.assertEqual(test_file.thumbnail_url, None)


def test_file_unzip(self): def test_file_unzip(self):
file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip') file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip')


+ 2
- 2
frappe/core/doctype/user/user.py ファイルの表示

@@ -253,8 +253,8 @@ class User(Document):
self.email_new_password(new_password) self.email_new_password(new_password)


except frappe.OutgoingEmailError: except frappe.OutgoingEmailError:
print(frappe.get_traceback())
pass # email server not set, don't send email
# email server not set, don't send email
frappe.log_error(frappe.get_traceback())


@Document.hook @Document.hook
def validate_reset_password(self): def validate_reset_password(self):


+ 54
- 34
frappe/database/database.py ファイルの表示

@@ -119,6 +119,9 @@ class Database(object):
if not run: if not run:
return query return query


# remove \n \t from start and end of query
query = re.sub(r'^\s*|\s*$', '', query)

if re.search(r'ifnull\(', query, flags=re.IGNORECASE): if re.search(r'ifnull\(', query, flags=re.IGNORECASE):
# replaces ifnull in query with coalesce # replaces ifnull in query with coalesce
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE) query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE)
@@ -384,7 +387,7 @@ class Database(object):
""" """


ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug, ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct)
order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct, limit=1)


if not run: if not run:
return ret return ret
@@ -393,7 +396,7 @@ class Database(object):


def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False, def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False,
run=True, pluck=False, distinct=False):
run=True, pluck=False, distinct=False, limit=None):
"""Returns multiple document properties. """Returns multiple document properties.


:param doctype: DocType name. :param doctype: DocType name.
@@ -423,14 +426,15 @@ class Database(object):


if isinstance(filters, list): if isinstance(filters, list):
out = self._get_value_for_many_names( out = self._get_value_for_many_names(
doctype,
filters,
fieldname,
order_by,
doctype=doctype,
names=filters,
field=fieldname,
order_by=order_by,
debug=debug, debug=debug,
run=run, run=run,
pluck=pluck, pluck=pluck,
distinct=distinct, distinct=distinct,
limit=limit,
) )


else: else:
@@ -444,17 +448,18 @@ class Database(object):
if order_by: if order_by:
order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by
out = self._get_values_from_table( out = self._get_values_from_table(
fields,
filters,
doctype,
as_dict,
debug,
order_by,
update,
fields=fields,
filters=filters,
doctype=doctype,
as_dict=as_dict,
debug=debug,
order_by=order_by,
update=update,
for_update=for_update, for_update=for_update,
run=run, run=run,
pluck=pluck, pluck=pluck,
distinct=distinct
distinct=distinct,
limit=limit,
) )
except Exception as e: except Exception as e:
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)): if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
@@ -623,6 +628,7 @@ class Database(object):
run=True, run=True,
pluck=False, pluck=False,
distinct=False, distinct=False,
limit=None,
): ):
field_objects = [] field_objects = []


@@ -641,6 +647,7 @@ class Database(object):
field_objects=field_objects, field_objects=field_objects,
fields=fields, fields=fields,
distinct=distinct, distinct=distinct,
limit=limit,
) )
if ( if (
fields == "*" fields == "*"
@@ -654,7 +661,7 @@ class Database(object):
) )
return r return r


def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False):
def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False, limit=None):
names = list(filter(None, names)) names = list(filter(None, names))
if names: if names:
return self.get_all( return self.get_all(
@@ -667,6 +674,7 @@ class Database(object):
as_list=1, as_list=1,
run=run, run=run,
distinct=distinct, distinct=distinct,
limit_page_length=limit
) )
else: else:
return {} return {}
@@ -882,27 +890,39 @@ class Database(object):
return self.sql("select name from `tab{doctype}` limit 1".format(doctype=doctype)) return self.sql("select name from `tab{doctype}` limit 1".format(doctype=doctype))


def exists(self, dt, dn=None, cache=False): def exists(self, dt, dn=None, cache=False):
"""Returns true if document exists.
"""Return the document name of a matching document, or None.


:param dt: DocType name.
:param dn: Document name or filter dict."""
if isinstance(dt, str):
if dt!="DocType" and dt==dn:
return True # single always exists (!)
try:
return self.get_value(dt, dn, "name", cache=cache)
except Exception:
return None
Note: `cache` only works if `dt` and `dn` are of type `str`.


elif isinstance(dt, dict) and dt.get('doctype'):
try:
conditions = []
for d in dt:
if d == 'doctype': continue
conditions.append([d, '=', dt[d]])
return self.get_all(dt['doctype'], filters=conditions, as_list=1)
except Exception:
return None
## Examples

Pass doctype and docname (only in this case we can cache the result)

```
exists("User", "jane@example.org", cache=True)
```

Pass a dict of filters including the `"doctype"` key:

```
exists({"doctype": "User", "full_name": "Jane Doe"})
```

Pass the doctype and a dict of filters:

```
exists("User", {"full_name": "Jane Doe"})
```
"""
if dt != "DocType" and dt == dn:
# single always exists (!)
return dn

if isinstance(dt, dict):
dt = dt.copy() # don't modify the original dict
dt, dn = dt.pop("doctype"), dt

return self.get_value(dt, dn, ignore=True, cache=cache)


def count(self, dt, filters=None, debug=False, cache=False): def count(self, dt, filters=None, debug=False, cache=False):
"""Returns `COUNT(*)` for given DocType and filters.""" """Returns `COUNT(*)` for given DocType and filters."""


+ 2
- 2
frappe/email/doctype/auto_email_report/auto_email_report.py ファイルの表示

@@ -15,8 +15,6 @@ from frappe.utils.csvutils import to_csv
from frappe.utils.xlsxutils import make_xlsx from frappe.utils.xlsxutils import make_xlsx
from frappe.desk.query_report import build_xlsx_data from frappe.desk.query_report import build_xlsx_data


max_reports_per_user = frappe.local.conf.max_reports_per_user or 3



class AutoEmailReport(Document): class AutoEmailReport(Document):
def autoname(self): def autoname(self):
@@ -46,6 +44,8 @@ class AutoEmailReport(Document):
def validate_report_count(self): def validate_report_count(self):
'''check that there are only 3 enabled reports per user''' '''check that there are only 3 enabled reports per user'''
count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0] count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0]
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3

if count > max_reports_per_user + (-1 if self.flags.in_insert else 0): if count > max_reports_per_user + (-1 if self.flags.in_insert else 0):
frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user)) frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user))




+ 1
- 1
frappe/email/doctype/notification/test_notification.py ファイルの表示

@@ -240,7 +240,7 @@ class TestNotification(unittest.TestCase):
self.assertTrue(email_queue) self.assertTrue(email_queue)


# check if description is changed after alert since set_property_after_alert is set # check if description is changed after alert since set_property_after_alert is set
self.assertEquals(todo.description, 'Changed by Notification')
self.assertEqual(todo.description, 'Changed by Notification')


recipients = [d.recipient for d in email_queue.recipients] recipients = [d.recipient for d in email_queue.recipients]
self.assertTrue('test2@example.com' in recipients) self.assertTrue('test2@example.com' in recipients)


+ 7
- 10
frappe/handler.py ファイルの表示

@@ -225,11 +225,10 @@ def ping():


def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
"""run a whitelisted controller method""" """run a whitelisted controller method"""
import json
import inspect
from inspect import getfullargspec


if not args:
args = arg or ""
if not args and arg:
args = arg


if dt: # not called from a doctype (from a page) if dt: # not called from a doctype (from a page)
if not dn: if not dn:
@@ -237,9 +236,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
doc = frappe.get_doc(dt, dn) doc = frappe.get_doc(dt, dn)


else: else:
if isinstance(docs, str):
docs = json.loads(docs)

docs = frappe.parse_json(docs)
doc = frappe.get_doc(docs) doc = frappe.get_doc(docs)
doc._original_modified = doc.modified doc._original_modified = doc.modified
doc.check_if_latest() doc.check_if_latest()
@@ -248,16 +245,16 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
throw_permission_error() throw_permission_error()


try: try:
args = json.loads(args)
args = frappe.parse_json(args)
except ValueError: except ValueError:
args = args
pass


method_obj = getattr(doc, method) method_obj = getattr(doc, method)
fn = getattr(method_obj, '__func__', method_obj) fn = getattr(method_obj, '__func__', method_obj)
is_whitelisted(fn) is_whitelisted(fn)
is_valid_http_method(fn) is_valid_http_method(fn)


fnargs = inspect.getfullargspec(method_obj).args
fnargs = getfullargspec(method_obj).args


if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"): if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"):
response = doc.run_method(method) response = doc.run_method(method)


+ 1
- 1
frappe/model/base_document.py ファイルの表示

@@ -963,7 +963,7 @@ class BaseDocument(object):
from frappe.model.meta import get_default_df from frappe.model.meta import get_default_df
df = get_default_df(fieldname) df = get_default_df(fieldname)


if not currency and df:
if df.fieldtype == "Currency" and not currency:
currency = self.get(df.get("options")) currency = self.get(df.get("options"))
if not frappe.db.exists('Currency', currency, cache=True): if not frappe.db.exists('Currency', currency, cache=True):
currency = None currency = None


+ 1
- 1
frappe/permissions.py ファイルの表示

@@ -594,4 +594,4 @@ def is_parent_valid(child_doctype, parent_doctype):
from frappe.core.utils import find from frappe.core.utils import find
parent_meta = frappe.get_meta(parent_doctype) parent_meta = frappe.get_meta(parent_doctype)
child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype) child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype)
return not parent_meta.istable and child_table_field_exists
return not parent_meta.istable and child_table_field_exists

+ 4
- 4
frappe/public/js/frappe/form/controls/attach.js ファイルの表示

@@ -37,8 +37,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro
if(this.frm) { if(this.frm) {
me.parse_validate_and_set_in_model(null); me.parse_validate_and_set_in_model(null);
me.refresh(); me.refresh();
me.frm.attachments.remove_attachment_by_filename(me.value, function() {
me.parse_validate_and_set_in_model(null);
me.frm.attachments.remove_attachment_by_filename(me.value, async () => {
await me.parse_validate_and_set_in_model(null);
me.refresh(); me.refresh();
me.frm.doc.docstatus == 1 ? me.frm.save('Update') : me.frm.save(); me.frm.doc.docstatus == 1 ? me.frm.save('Update') : me.frm.save();
}); });
@@ -110,9 +110,9 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro
return this.value || null; return this.value || null;
} }


on_upload_complete(attachment) {
async on_upload_complete(attachment) {
if(this.frm) { if(this.frm) {
this.parse_validate_and_set_in_model(attachment.file_url);
await this.parse_validate_and_set_in_model(attachment.file_url);
this.frm.attachments.update_attachment(attachment); this.frm.attachments.update_attachment(attachment);
this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save(); this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save();
} }


+ 4
- 1
frappe/public/js/frappe/form/footer/form_timeline.js ファイルの表示

@@ -454,7 +454,10 @@ class FormTimeline extends BaseTimeline {
let edit_box = this.make_editable(edit_wrapper); let edit_box = this.make_editable(edit_wrapper);
let content_wrapper = comment_wrapper.find('.content'); let content_wrapper = comment_wrapper.find('.content');
let more_actions_wrapper = comment_wrapper.find('.more-actions'); let more_actions_wrapper = comment_wrapper.find('.more-actions');
if (frappe.model.can_delete("Comment")) {
if (frappe.model.can_delete("Comment") && (
frappe.session.user == doc.owner ||
frappe.user.has_role("System Manager")
)) {
const delete_option = $(` const delete_option = $(`
<li> <li>
<a class="dropdown-item"> <a class="dropdown-item">


+ 1
- 1
frappe/public/js/frappe/list/list_settings.js ファイルの表示

@@ -375,7 +375,7 @@ export default class ListSettings {
let me = this; let me = this;


if (me.removed_fields) { if (me.removed_fields) {
me.removed_fields.concat(fields);
me.removed_fields = me.removed_fields.concat(fields);
} else { } else {
me.removed_fields = fields; me.removed_fields = fields;
} }


+ 9
- 5
frappe/public/js/frappe/ui/page.js ファイルの表示

@@ -47,13 +47,17 @@ frappe.ui.Page = class Page {
} }


setup_scroll_handler() { setup_scroll_handler() {
window.addEventListener('scroll', () => {
if (document.documentElement.scrollTop) {
$('.page-head').toggleClass('drop-shadow', true);
let last_scroll = 0;
window.addEventListener('scroll', frappe.utils.throttle(() => {
$('.page-head').toggleClass('drop-shadow', !!document.documentElement.scrollTop);
let current_scroll = document.documentElement.scrollTop;
if (current_scroll > 0 && last_scroll <= current_scroll) {
$('.page-head').css("top", "-15px");
} else { } else {
$('.page-head').removeClass('drop-shadow');
$('.page-head').css("top", "var(--navbar-height)");
} }
});
last_scroll = current_scroll;
}), 500);
} }


get_empty_state(title, message, primary_action) { get_empty_state(title, message, primary_action) {


+ 1
- 1
frappe/public/js/frappe/utils/utils.js ファイルの表示

@@ -231,7 +231,7 @@ Object.assign(frappe.utils, {
if (tt && (tt.substr(0, 1)===">" || tt.substr(0, 4)==="&gt;")) { if (tt && (tt.substr(0, 1)===">" || tt.substr(0, 4)==="&gt;")) {
part.push(t); part.push(t);
} else { } else {
out.concat(part);
out = out.concat(part);
out.push(t); out.push(t);
part = []; part = [];
} }


+ 1
- 1
frappe/public/js/frappe/views/reports/report_view.js ファイルの表示

@@ -1026,7 +1026,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
} }
if (!docfield || docfield.report_hide) return; if (!docfield || docfield.report_hide) return;


let title = __(docfield ? docfield.label : toTitle(fieldname));
let title = __(docfield.label);
if (doctype !== this.doctype) { if (doctype !== this.doctype) {
title += ` (${__(doctype)})`; title += ` (${__(doctype)})`;
} }


+ 36
- 6
frappe/public/js/frappe/web_form/web_form_list.js ファイルの表示

@@ -16,7 +16,8 @@ export default class WebFormList {
if (this.table) { if (this.table) {
Array.from(this.table.tBodies).forEach(tbody => tbody.remove()); Array.from(this.table.tBodies).forEach(tbody => tbody.remove());
let check = document.getElementById('select-all'); let check = document.getElementById('select-all');
check.checked = false;
if (check)
check.checked = false;
} }
this.rows = []; this.rows = [];
this.page_length = 20; this.page_length = 20;
@@ -131,9 +132,39 @@ export default class WebFormList {
this.make_table_head(); this.make_table_head();
} }


this.append_rows(this.data);

this.wrapper.appendChild(this.table);
if (this.data.length) {
this.append_rows(this.data);
this.wrapper.appendChild(this.table);
} else {
let new_button = "";
let empty_state = document.createElement("div");
empty_state.classList.add("no-result", "text-muted", "flex", "justify-center", "align-center");

frappe.has_permission(this.doctype, "", "create", () => {
new_button = `
<a
class="btn btn-primary btn-sm btn-new-doc hidden-xs"
href="${window.location.pathname}?new=1">
${__("Create a new {0}", [__(this.doctype)])}
</a>
`;

empty_state.innerHTML = `
<div class="text-center">
<div>
<img
src="/assets/frappe/images/ui-states/list-empty-state.svg"
alt="Generic Empty State"
class="null-state">
</div>
<p class="small mb-2">${__("No {0} found", [__(this.doctype)])}</p>
${new_button}
</div>
`;

this.wrapper.appendChild(empty_state);
});
}
} }


make_table_head() { make_table_head() {
@@ -212,8 +243,7 @@ export default class WebFormList {
"btn", "btn",
"btn-secondary", "btn-secondary",
"btn-sm", "btn-sm",
"ml-2",
"text-white"
"ml-2"
); );
} }
else if (type == "danger") { else if (type == "danger") {


+ 1
- 0
frappe/public/scss/desk/page.scss ファイルの表示

@@ -88,6 +88,7 @@
top: var(--navbar-height); top: var(--navbar-height);
background: var(--bg-color); background: var(--bg-color);
margin-bottom: 5px; margin-bottom: 5px;
transition: 0.5s top;
.page-head-content { .page-head-content {
height: var(--page-head-height); height: var(--page-head-height);
} }


+ 13
- 0
frappe/public/scss/website/index.scss ファイルの表示

@@ -311,3 +311,16 @@ h5.modal-title {
.empty-list-icon { .empty-list-icon {
height: 70px; height: 70px;
} }

.null-state {
height: 60px;
width: auto;
margin-bottom: var(--margin-md);
img {
fill: var(--fg-color);
}
}

.no-result {
min-height: #{"calc(100vh - 284px)"};
}

+ 3
- 3
frappe/tests/test_base_document.py ファイルの表示

@@ -7,12 +7,12 @@ class TestBaseDocument(unittest.TestCase):
def test_docstatus(self): def test_docstatus(self):
doc = BaseDocument({"docstatus": 0}) doc = BaseDocument({"docstatus": 0})
self.assertTrue(doc.docstatus.is_draft()) self.assertTrue(doc.docstatus.is_draft())
self.assertEquals(doc.docstatus, 0)
self.assertEqual(doc.docstatus, 0)


doc.docstatus = 1 doc.docstatus = 1
self.assertTrue(doc.docstatus.is_submitted()) self.assertTrue(doc.docstatus.is_submitted())
self.assertEquals(doc.docstatus, 1)
self.assertEqual(doc.docstatus, 1)


doc.docstatus = 2 doc.docstatus = 2
self.assertTrue(doc.docstatus.is_cancelled()) self.assertTrue(doc.docstatus.is_cancelled())
self.assertEquals(doc.docstatus, 2)
self.assertEqual(doc.docstatus, 2)

+ 38
- 3
frappe/tests/test_db.py ファイルの表示

@@ -14,7 +14,7 @@ from frappe.database.database import Database
from frappe.query_builder import Field from frappe.query_builder import Field
from frappe.query_builder.functions import Concat_ws from frappe.query_builder.functions import Concat_ws
from frappe.tests.test_query_builder import db_type_is, run_only_if from frappe.tests.test_query_builder import db_type_is, run_only_if
from frappe.utils import add_days, now, random_string
from frappe.utils import add_days, now, random_string, cint
from frappe.utils.testutils import clear_custom_fields from frappe.utils.testutils import clear_custom_fields




@@ -84,6 +84,27 @@ class TestDB(unittest.TestCase):
), ),
) )


def test_get_value_limits(self):

# check both dict and list style filters
filters = [{"enabled": 1}, [["enabled", "=", 1]]]
for filter in filters:
self.assertEqual(1, len(frappe.db.get_values("User", filters=filter, limit=1)))
# count of last touched rows as per DB-API 2.0 https://peps.python.org/pep-0249/#rowcount
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount))
self.assertEqual(2, len(frappe.db.get_values("User", filters=filter, limit=2)))
self.assertGreaterEqual(2, cint(frappe.db._cursor.rowcount))

# without limits length == count
self.assertEqual(len(frappe.db.get_values("User", filters=filter)),
frappe.db.count("User", filter))

frappe.db.get_value("User", filters=filter)
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount))

frappe.db.exists("User", filter)
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount))

def test_escape(self): def test_escape(self):
frappe.db.escape("香港濟生堂製藥有限公司 - IT".encode("utf-8")) frappe.db.escape("香港濟生堂製藥有限公司 - IT".encode("utf-8"))


@@ -301,6 +322,20 @@ class TestDB(unittest.TestCase):
# recover transaction to continue other tests # recover transaction to continue other tests
raise Exception raise Exception


def test_exists(self):
dt, dn = "User", "Administrator"
self.assertEqual(frappe.db.exists(dt, dn, cache=True), dn)
self.assertEqual(frappe.db.exists(dt, dn), dn)
self.assertEqual(frappe.db.exists(dt, {"name": ("=", dn)}), dn)

filters = {"doctype": dt, "name": ("like", "Admin%")}
self.assertEqual(frappe.db.exists(filters), dn)
self.assertEqual(
filters["doctype"], dt
) # make sure that doctype was not removed from filters

self.assertEqual(frappe.db.exists(dt, [["name", "=", dn]]), dn)



@run_only_if(db_type_is.MARIADB) @run_only_if(db_type_is.MARIADB)
class TestDDLCommandsMaria(unittest.TestCase): class TestDDLCommandsMaria(unittest.TestCase):
@@ -357,7 +392,7 @@ class TestDDLCommandsMaria(unittest.TestCase):
WHERE Key_name = '{index_name}'; WHERE Key_name = '{index_name}';
""" """
) )
self.assertEquals(len(indexs_in_table), 2)
self.assertEqual(len(indexs_in_table), 2)




class TestDBSetValue(unittest.TestCase): class TestDBSetValue(unittest.TestCase):
@@ -561,7 +596,7 @@ class TestDDLCommandsPost(unittest.TestCase):
AND indexname = '{index_name}' ; AND indexname = '{index_name}' ;
""", """,
) )
self.assertEquals(len(indexs_in_table), 1)
self.assertEqual(len(indexs_in_table), 1)


@run_only_if(db_type_is.POSTGRES) @run_only_if(db_type_is.POSTGRES)
def test_modify_query(self): def test_modify_query(self):


+ 3
- 3
frappe/tests/test_document.py ファイルの表示

@@ -260,15 +260,15 @@ class TestDocument(unittest.TestCase):
'doctype': 'Test Formatted', 'doctype': 'Test Formatted',
'currency': 100000 'currency': 100000
}) })
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')
self.assertEqual(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')


def test_limit_for_get(self): def test_limit_for_get(self):
doc = frappe.get_doc("DocType", "DocType") doc = frappe.get_doc("DocType", "DocType")
# assuming DocType has more than 3 Data fields # assuming DocType has more than 3 Data fields
self.assertEquals(len(doc.get("fields", limit=3)), 3)
self.assertEqual(len(doc.get("fields", limit=3)), 3)


# limit with filters # limit with filters
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)
self.assertEqual(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)


def test_virtual_fields(self): def test_virtual_fields(self):
"""Virtual fields are accessible via API and Form views, whenever .as_dict is invoked """Virtual fields are accessible via API and Form views, whenever .as_dict is invoked


+ 2
- 2
frappe/tests/test_search.py ファイルの表示

@@ -70,10 +70,10 @@ class TestSearch(unittest.TestCase):
result = frappe.response['results'] result = frappe.response['results']


# Check whether the result is sorted or not # Check whether the result is sorted or not
self.assertEquals(self.parent_doctype_name, result[0]['value'])
self.assertEqual(self.parent_doctype_name, result[0]['value'])


# Check whether searching for parent also list out children # Check whether searching for parent also list out children
self.assertEquals(len(result), len(self.child_doctypes_names) + 1)
self.assertEqual(len(result), len(self.child_doctypes_names) + 1)


#Search for the word "pay", part of the word "pays" (country) in french. #Search for the word "pay", part of the word "pays" (country) in french.
def test_link_search_in_foreign_language(self): def test_link_search_in_foreign_language(self):


+ 0
- 2
frappe/translate.py ファイルの表示

@@ -650,8 +650,6 @@ def extract_messages_from_code(code):
if isinstance(e, InvalidIncludePath): if isinstance(e, InvalidIncludePath):
frappe.clear_last_message() frappe.clear_last_message()


pass

messages = [] messages = []
pattern = r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)" pattern = r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)"




+ 3
- 3
frappe/utils/backups.py ファイルの表示

@@ -15,7 +15,7 @@ import click


# imports - module imports # imports - module imports
import frappe import frappe
from frappe import _, conf
from frappe import conf
from frappe.utils import get_file_size, get_url, now, now_datetime, cint from frappe.utils import get_file_size, get_url, now, now_datetime, cint
from frappe.utils.password import get_encryption_key from frappe.utils.password import get_encryption_key


@@ -505,7 +505,7 @@ download only after 24 hours.""" % {
datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded""" datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded"""
) )


frappe.sendmail(recipients=recipient_list, msg=msg, subject=subject)
frappe.sendmail(recipients=recipient_list, message=msg, subject=subject)
return recipient_list return recipient_list




@@ -779,7 +779,7 @@ if __name__ == "__main__":
db_type=db_type, db_type=db_type,
db_port=db_port, db_port=db_port,
) )
odb.send_email("abc.sql.gz")
odb.send_email()


if cmd == "delete_temp_backups": if cmd == "delete_temp_backups":
delete_temp_backups() delete_temp_backups()

+ 0
- 1
frappe/utils/nestedset.py ファイルの表示

@@ -227,7 +227,6 @@ class NestedSet(Document):
update_nsm(self) update_nsm(self)
except frappe.DoesNotExistError: except frappe.DoesNotExistError:
if self.flags.on_rollback: if self.flags.on_rollback:
pass
frappe.message_log.pop() frappe.message_log.pop()
else: else:
raise raise


読み込み中…
キャンセル
保存