Ver código fonte

Merge branch 'develop' of github.com:frappe/frappe into update-title-types

version-14
Gavin D'souza 3 anos atrás
pai
commit
f8b52d8e4f
62 arquivos alterados com 604 adições e 579 exclusões
  1. +3
    -0
      .git-blame-ignore-revs
  2. +10
    -4
      .github/helper/roulette.py
  3. +2
    -1
      .github/workflows/ui-tests.yml
  4. +1
    -1
      README.md
  5. +21
    -0
      cypress/integration/depends_on.js
  6. +1
    -0
      cypress/integration/list_view.js
  7. +2
    -1
      cypress/integration/report_view.js
  8. +14
    -133
      frappe/build.py
  9. +4
    -8
      frappe/commands/site.py
  10. +7
    -0
      frappe/core/doctype/doctype/doctype.js
  11. +13
    -5
      frappe/core/doctype/doctype/doctype.py
  12. +7
    -0
      frappe/core/doctype/doctype/test_doctype.py
  13. +53
    -1
      frappe/core/doctype/report/test_report.py
  14. +2
    -2
      frappe/custom/doctype/customize_form/test_customize_form.py
  15. +1
    -1
      frappe/database/database.py
  16. +12
    -10
      frappe/database/postgres/schema.py
  17. +20
    -9
      frappe/desk/form/meta.py
  18. +1
    -1
      frappe/desk/page/setup_wizard/setup_wizard.py
  19. +17
    -7
      frappe/desk/query_report.py
  20. +3
    -2
      frappe/desk/reportview.py
  21. +1
    -1
      frappe/desk/treeview.py
  22. +1
    -1
      frappe/desk/utils.py
  23. +5
    -3
      frappe/email/doctype/email_domain/test_email_domain.py
  24. +2
    -3
      frappe/installer.py
  25. +2
    -2
      frappe/integrations/doctype/ldap_settings/ldap_settings.py
  26. +130
    -59
      frappe/migrate.py
  27. +8
    -4
      frappe/model/base_document.py
  28. +2
    -2
      frappe/model/db_query.py
  29. +1
    -1
      frappe/model/delete_doc.py
  30. +1
    -1
      frappe/model/rename_doc.py
  31. +1
    -1
      frappe/model/sync.py
  32. +3
    -2
      frappe/modules/import_file.py
  33. +3
    -1
      frappe/public/js/frappe/form/controls/date.js
  34. +3
    -1
      frappe/public/js/frappe/form/form.js
  35. +1
    -1
      frappe/public/js/frappe/form/grid.js
  36. +5
    -5
      frappe/public/js/frappe/form/grid_row.js
  37. +8
    -6
      frappe/public/js/frappe/form/layout.js
  38. +12
    -13
      frappe/public/js/frappe/form/multi_select_dialog.js
  39. +1
    -1
      frappe/public/js/frappe/form/tab.js
  40. +24
    -1
      frappe/public/js/frappe/list/base_list.js
  41. +1
    -1
      frappe/public/js/frappe/list/list_view.js
  42. +2
    -2
      frappe/public/js/frappe/model/meta.js
  43. +3
    -1
      frappe/public/js/frappe/model/model.js
  44. +2
    -2
      frappe/public/js/frappe/ui/filters/filter_list.js
  45. +3
    -2
      frappe/public/js/frappe/views/reports/query_report.js
  46. +1
    -1
      frappe/public/js/frappe/views/reports/report_view.js
  47. +6
    -4
      frappe/public/scss/common/color_picker.scss
  48. +1
    -1
      frappe/public/scss/common/grid.scss
  49. +5
    -0
      frappe/public/scss/common/modal.scss
  50. +2
    -2
      frappe/public/scss/desk/frappe_datatable.scss
  51. +1
    -1
      frappe/realtime.py
  52. +3
    -3
      frappe/test_runner.py
  53. +133
    -25
      frappe/tests/test_commands.py
  54. +4
    -2
      frappe/translate.py
  55. +3
    -4
      frappe/translations/de.csv
  56. +20
    -15
      frappe/utils/background_jobs.py
  57. +1
    -1
      frappe/utils/install.py
  58. +0
    -212
      frappe/utils/minify.py
  59. +1
    -1
      frappe/utils/pdf.py
  60. +2
    -2
      frappe/utils/redis_wrapper.py
  61. +1
    -1
      frappe/utils/user.py
  62. +1
    -1
      frappe/website/utils.py

+ 3
- 0
.git-blame-ignore-revs Ver arquivo

@@ -13,3 +13,6 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85

# Updating license headers
34460265554242a8d05fb09f049033b1117e1a2b

# Refactor "not a in b" -> "a not in b"
745297a49d516e5e3c4bb3e1b0c4235e7d31165d

+ 10
- 4
.github/helper/roulette.py Ver arquivo

@@ -41,6 +41,7 @@ if __name__ == "__main__":
# this is a push build, run all builds
if not pr_number:
os.system('echo "::set-output name=build::strawberry"')
os.system('echo "::set-output name=build-server::strawberry"')
sys.exit(0)

files_list = files_list or get_files_list(pr_number=pr_number, repo=repo)
@@ -52,7 +53,8 @@ if __name__ == "__main__":
ci_files_changed = any(f for f in files_list if is_ci(f))
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
updated_py_file_count = len(list(filter(is_py, files_list)))
only_py_changed = updated_py_file_count == len(files_list)

if ci_files_changed:
print("CI related files were updated, running all build processes.")
@@ -65,8 +67,12 @@ if __name__ == "__main__":
print("Only Frontend code was updated; Stopping Python build process.")
sys.exit(0)

elif only_py_changed and build_type == "ui":
print("Only Python code was updated, stopping Cypress build process.")
sys.exit(0)
elif build_type == "ui":
if only_py_changed:
print("Only Python code was updated, stopping Cypress build process.")
sys.exit(0)
elif updated_py_file_count > 0:
# both frontend and backend code were updated
os.system('echo "::set-output name=build-server::strawberry"')

os.system('echo "::set-output name=build::strawberry"')

+ 2
- 1
.github/workflows/ui-tests.yml Ver arquivo

@@ -142,6 +142,7 @@ jobs:
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb

- name: Stop server
if: ${{ steps.check-build.outputs.build-server == 'strawberry' }}
run: |
ps -ef | grep "frappe serve" | awk '{print $2}' | xargs kill -s SIGINT 2> /dev/null || true
sleep 5
@@ -163,7 +164,7 @@ jobs:
flags: ui-tests

- name: Upload Server Coverage Data
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
if: ${{ steps.check-build.outputs.build-server == 'strawberry' }}
uses: codecov/codecov-action@v2
with:
name: MariaDB


+ 1
- 1
README.md Ver arquivo

@@ -27,7 +27,7 @@
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
</a>
<a href="https://codecov.io/gh/frappe/frappe">
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj&flag=server"/>
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj"/>
</a>
</div>



+ 21
- 0
cypress/integration/depends_on.js Ver arquivo

@@ -55,10 +55,31 @@ context('Depends On', () => {
'read_only_depends_on': "eval:doc.test_field=='Some Other Value'",
'options': "Child Test Depends On"
},
{
"label": "Dependent Tab",
"fieldname": "dependent_tab",
"fieldtype": "Tab Break",
"depends_on": "eval:doc.test_field=='Show Tab'"
},
{
"fieldname": "tab_section",
"fieldtype": "Section Break",
},
{
"label": "Field in Tab",
"fieldname": "field_in_tab",
"fieldtype": "Data",
}
]
});
});
});
it('should show the tab on other setting field value', () => {
cy.new_form('Test Depends On');
cy.fill_field('test_field', 'Show Tab');
cy.get('body').click();
cy.findByRole("tab", {name: "Dependent Tab"}).should('be.visible');
});
it('should set the field as mandatory depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.fill_field('test_field', 'Some Value');


+ 1
- 0
cypress/integration/list_view.js Ver arquivo

@@ -12,6 +12,7 @@ context('List View', () => {
cy.get('.list-row-container .list-row-checkbox').click({ multiple: true, force: true });
cy.get('.actions-btn-group button').contains('Actions').should('be.visible');
cy.intercept('/api/method/frappe.desk.reportview.get').as('list-refresh');
cy.wait(3000); // wait before you hit another refresh
cy.get('button[data-original-title="Refresh"]').click();
cy.wait('@list-refresh');
cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible');


+ 2
- 1
cypress/integration/report_view.js Ver arquivo

@@ -29,6 +29,7 @@ context('Report View', () => {
// select the cell
cell.dblclick();
cell.get('.dt-cell__edit--col-4').findByRole('checkbox').check({ force: true });
cy.get('.dt-row-0 > .dt-cell--col-3').click(); // click outside

cy.wait('@value-update');

@@ -70,4 +71,4 @@ context('Report View', () => {
cy.get('.list-paging-area .btn-more').click();
cy.get('.list-paging-area .list-count').should('contain.text', '1000 of');
});
});
});

+ 14
- 133
frappe/build.py Ver arquivo

@@ -1,25 +1,21 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import os
import re
import json
import shutil
import re
import subprocess
from distutils.spawn import find_executable
from subprocess import getoutput
from io import StringIO
from tempfile import mkdtemp, mktemp
from distutils.spawn import find_executable

import frappe
from frappe.utils.minify import JavascriptMinify
from urllib.parse import urlparse

import click
import psutil
from urllib.parse import urlparse
from semantic_version import Version
from requests import head
from requests.exceptions import HTTPError
from semantic_version import Version

import frappe

timestamps = {}
app_paths = None
@@ -32,6 +28,7 @@ class AssetsNotDownloadedError(Exception):
class AssetsDontExistError(HTTPError):
pass


def download_file(url, prefix):
from requests import get

@@ -277,12 +274,14 @@ def check_node_executable():
click.echo(f"{warn} Please install yarn using below command and try again.\nnpm install -g yarn")
click.echo()


def get_node_env():
node_env = {
"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"
}
return node_env


def get_safe_max_old_space_size():
safe_max_old_space_size = 0
try:
@@ -296,6 +295,7 @@ def get_safe_max_old_space_size():

return safe_max_old_space_size


def generate_assets_map():
symlinks = {}

@@ -344,7 +344,6 @@ def clear_broken_symlinks():
os.remove(path)



def unstrip(message: str) -> str:
"""Pads input string on the right side until the last available column in the terminal
"""
@@ -397,94 +396,6 @@ def link_assets_dir(source, target, hard_link=False):
symlink(source, target, overwrite=True)


def build(no_compress=False, verbose=False):
for target, sources in get_build_maps().items():
pack(os.path.join(assets_path, target), sources, no_compress, verbose)


def get_build_maps():
"""get all build.jsons with absolute paths"""
# framework js and css files

build_maps = {}
for app_path in app_paths:
path = os.path.join(app_path, "public", "build.json")
if os.path.exists(path):
with open(path) as f:
try:
for target, sources in (json.loads(f.read() or "{}")).items():
# update app path
source_paths = []
for source in sources:
if isinstance(source, list):
s = frappe.get_pymodule_path(source[0], *source[1].split("/"))
else:
s = os.path.join(app_path, source)
source_paths.append(s)

build_maps[target] = source_paths
except ValueError as e:
print(path)
print("JSON syntax error {0}".format(str(e)))
return build_maps


def pack(target, sources, no_compress, verbose):
outtype, outtxt = target.split(".")[-1], ""
jsm = JavascriptMinify()

for f in sources:
suffix = None
if ":" in f:
f, suffix = f.split(":")
if not os.path.exists(f) or os.path.isdir(f):
print("did not find " + f)
continue
timestamps[f] = os.path.getmtime(f)
try:
with open(f, "r") as sourcefile:
data = str(sourcefile.read(), "utf-8", errors="ignore")

extn = f.rsplit(".", 1)[1]

if (
outtype == "js"
and extn == "js"
and (not no_compress)
and suffix != "concat"
and (".min." not in f)
):
tmpin, tmpout = StringIO(data.encode("utf-8")), StringIO()
jsm.minify(tmpin, tmpout)
minified = tmpout.getvalue()
if minified:
outtxt += str(minified or "", "utf-8").strip("\n") + ";"

if verbose:
print("{0}: {1}k".format(f, int(len(minified) / 1024)))
elif outtype == "js" and extn == "html":
# add to frappe.templates
outtxt += html_to_js_template(f, data)
else:
outtxt += "\n/*\n *\t%s\n */" % f
outtxt += "\n" + data + "\n"

except Exception:
print("--Error in:" + f + "--")
print(frappe.get_traceback())

with open(target, "w") as f:
f.write(outtxt.encode("utf-8"))

print("Wrote %s - %sk" % (target, str(int(os.path.getsize(target) / 1024))))


def html_to_js_template(path, content):
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
return """frappe.templates["{key}"] = '{content}';\n""".format(
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content))


def scrub_html_template(content):
"""Returns HTML content with removed whitespace and comments"""
# remove whitespace to a single space
@@ -496,37 +407,7 @@ def scrub_html_template(content):
return content.replace("'", "\'")


def files_dirty():
for target, sources in get_build_maps().items():
for f in sources:
if ":" in f:
f, suffix = f.split(":")
if not os.path.exists(f) or os.path.isdir(f):
continue
if os.path.getmtime(f) != timestamps.get(f):
print(f + " dirty")
return True
else:
return False


def compile_less():
if not find_executable("lessc"):
return

for path in app_paths:
less_path = os.path.join(path, "public", "less")
if os.path.exists(less_path):
for fname in os.listdir(less_path):
if fname.endswith(".less") and fname != "variables.less":
fpath = os.path.join(less_path, fname)
mtime = os.path.getmtime(fpath)
if fpath in timestamps and mtime == timestamps[fpath]:
continue

timestamps[fpath] = mtime

print("compiling {0}".format(fpath))

css_path = os.path.join(path, "public", "css", fname.rsplit(".", 1)[0] + ".css")
os.system("lessc {0} > {1}".format(fpath, css_path))
def html_to_js_template(path, content):
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
return """frappe.templates["{key}"] = '{content}';\n""".format(
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content))

+ 4
- 8
frappe/commands/site.py Ver arquivo

@@ -447,21 +447,17 @@ def disable_user(context, email):
@pass_context
def migrate(context, skip_failing=False, skip_search_index=False):
"Run patches, sync schema and rebuild files/translations"
from frappe.migrate import migrate
from frappe.migrate import SiteMigration

for site in context.sites:
click.secho(f"Migrating {site}", fg="green")
frappe.init(site=site)
frappe.connect()
try:
migrate(
context.verbose,
SiteMigration(
skip_failing=skip_failing,
skip_search_index=skip_search_index
)
skip_search_index=skip_search_index,
).run(site=site)
finally:
print()
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError



+ 7
- 0
frappe/core/doctype/doctype/doctype.js Ver arquivo

@@ -33,9 +33,16 @@ frappe.ui.form.on('DocType', {
}
}

const customize_form_link = "<a href='/app/customize-form'>Customize Form</a>";
if(!frappe.boot.developer_mode && !frm.doc.custom) {
// make the document read-only
frm.set_read_only();
frm.dashboard.add_comment(__("DocTypes can not be modified, please use {0} instead", [customize_form_link]), "blue", true);
} else if (frappe.boot.developer_mode) {
let msg = __("This site is running in developer mode. Any change made here will be updated in code.");
msg += "<br>";
msg += __("If you just want to customize for your site, use {0} instead.", [customize_form_link]);
frm.dashboard.add_comment(msg, "yellow");
}

if(frm.is_new()) {


+ 13
- 5
frappe/core/doctype/doctype/doctype.py Ver arquivo

@@ -786,9 +786,10 @@ def validate_links_table_fieldnames(meta):

fieldnames = tuple(field.fieldname for field in meta.fields)
for index, link in enumerate(meta.links, 1):
link_meta = frappe.get_meta(link.link_doctype)
if not link_meta.get_field(link.link_fieldname):
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
if not frappe.get_meta(link.link_doctype).has_field(link.link_fieldname):
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(
index, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype)
)
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))

if not link.is_child_table:
@@ -802,8 +803,15 @@ def validate_links_table_fieldnames(meta):
message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index)
frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))

if link.table_fieldname not in fieldnames:
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
if meta.name == link.parent_doctype:
field_exists = link.table_fieldname in fieldnames
else:
field_exists = frappe.get_meta(link.parent_doctype).has_field(link.table_fieldname)

if not field_exists:
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(
index, frappe.bold(link.table_fieldname), frappe.bold(meta.name)
)
frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname"))

def validate_fields_for_doctype(doctype):


+ 7
- 0
frappe/core/doctype/doctype/test_doctype.py Ver arquivo

@@ -498,6 +498,13 @@ class TestDocType(unittest.TestCase):
self.assertEqual(doc.is_virtual, 1)
self.assertFalse(frappe.db.table_exists('Test Virtual Doctype'))

def test_default_fieldname(self):
fields = [{"label": "title", "fieldname": "title", "fieldtype": "Data", "default": "{some_fieldname}"}]
dt = new_doctype("DT with default field", fields=fields)
dt.insert()

dt.delete()

def new_doctype(name, unique=0, depends_on='', fields=None):
doc = frappe.get_doc({
"doctype": "DocType",


+ 53
- 1
frappe/core/doctype/report/test_report.py Ver arquivo

@@ -3,7 +3,7 @@

import frappe, json, os
import unittest
from frappe.desk.query_report import run, save_report
from frappe.desk.query_report import run, save_report, add_total_row
from frappe.desk.reportview import delete_report, save_report as _save_report
from frappe.custom.doctype.customize_form.customize_form import reset_customization
from frappe.core.doctype.user_permission.test_user_permission import create_user
@@ -282,3 +282,55 @@ result = [

# Set user back to administrator
frappe.set_user('Administrator')

def test_add_total_row_for_tree_reports(self):
report_settings = {
'tree': True,
'parent_field': 'parent_value'
}

columns = [
{
"fieldname": "parent_column",
"label": "Parent Column",
"fieldtype": "Data",
"width": 10
},
{
"fieldname": "column_1",
"label": "Column 1",
"fieldtype": "Float",
"width": 10
},
{
"fieldname": "column_2",
"label": "Column 2",
"fieldtype": "Float",
"width": 10
}
]

result = [
{
"parent_column": "Parent 1",
"column_1": 200,
"column_2": 150.50
},
{
"parent_column": "Child 1",
"column_1": 100,
"column_2": 75.25,
"parent_value": "Parent 1"
},
{
"parent_column": "Child 2",
"column_1": 100,
"column_2": 75.25,
"parent_value": "Parent 1"
}
]

result = add_total_row(result, columns, meta=None, report_settings=report_settings)
self.assertEqual(result[-1][0], "Total")
self.assertEqual(result[-1][1], 200)
self.assertEqual(result[-1][2], 150.50)

+ 2
- 2
frappe/custom/doctype/customize_form/test_customize_form.py Ver arquivo

@@ -257,7 +257,7 @@ class TestCustomizeForm(unittest.TestCase):
frappe.clear_cache()
d = self.get_customize_form("User Group")

d.append('links', dict(link_doctype='User Group Member', parent_doctype='User',
d.append('links', dict(link_doctype='User Group Member', parent_doctype='User Group',
link_fieldname='user', table_fieldname='user_group_members', group='Tests', custom=1))

d.run_method("save_customization")
@@ -267,7 +267,7 @@ class TestCustomizeForm(unittest.TestCase):

# check links exist
self.assertTrue([d.name for d in user_group.links if d.link_doctype == 'User Group Member'])
self.assertTrue([d.name for d in user_group.links if d.parent_doctype == 'User'])
self.assertTrue([d.name for d in user_group.links if d.parent_doctype == 'User Group'])

# remove the link
d = self.get_customize_form("User Group")


+ 1
- 1
frappe/database/database.py Ver arquivo

@@ -584,7 +584,7 @@ class Database(object):
company = frappe.db.get_single_value('Global Defaults', 'default_company')
"""

if not doctype in self.value_cache:
if doctype not in self.value_cache:
self.value_cache[doctype] = {}

if cache and fieldname in self.value_cache[doctype]:


+ 12
- 10
frappe/database/postgres/schema.py Ver arquivo

@@ -5,29 +5,29 @@ from frappe.database.schema import DBTable, get_definition

class PostgresTable(DBTable):
def create(self):
add_text = ""
varchar_len = frappe.db.VARCHAR_LEN

additional_definitions = ""
# columns
column_defs = self.get_column_definitions()
if column_defs:
add_text += ",\n".join(column_defs)
additional_definitions += ",\n".join(column_defs)

# child table columns
if self.meta.get("istable") or 0:
if column_defs:
add_text += ",\n"
additional_definitions += ",\n"

add_text += ",\n".join(
additional_definitions += ",\n".join(
(
"parent varchar({varchar_len})",
"parentfield varchar({varchar_len})",
"parenttype varchar({varchar_len})"
f"parent varchar({varchar_len})",
f"parentfield varchar({varchar_len})",
f"parenttype varchar({varchar_len})",
)
)

# TODO: set docstatus length
# create table
frappe.db.sql(("""create table `%s` (
frappe.db.sql(f"""create table `{self.table_name}` (
name varchar({varchar_len}) not null primary key,
creation timestamp(6),
modified timestamp(6),
@@ -35,7 +35,9 @@ class PostgresTable(DBTable):
owner varchar({varchar_len}),
docstatus smallint not null default '0',
idx bigint not null default '0',
%s)""" % (self.table_name, add_text)).format(varchar_len=frappe.db.VARCHAR_LEN))
{additional_definitions}
)"""
)

self.create_indexes()
frappe.db.commit()


+ 20
- 9
frappe/desk/form/meta.py Ver arquivo

@@ -12,6 +12,15 @@ from frappe.translate import extract_messages_from_code, make_dict_from_messages
from frappe.utils import get_html_format


ASSET_KEYS = (
"__js", "__css", "__list_js", "__calendar_js", "__map_js",
"__linked_with", "__messages", "__print_formats", "__workflow_docs",
"__form_grid_templates", "__listview_template", "__tree_js",
"__dashboard", "__kanban_column_fields", '__templates',
'__custom_js', '__custom_list_js'
)


def get_meta(doctype, cached=True):
# don't cache for developer mode as js files, templates may be edited
if cached and not frappe.conf.developer_mode:
@@ -34,6 +43,12 @@ class FormMeta(Meta):
super(FormMeta, self).__init__(doctype)
self.load_assets()

def set(self, key, value, *args, **kwargs):
if key in ASSET_KEYS:
self.__dict__[key] = value
else:
super(FormMeta, self).set(key, value, *args, **kwargs)

def load_assets(self):
if self.get('__assets_loaded', False):
return
@@ -55,11 +70,7 @@ class FormMeta(Meta):
def as_dict(self, no_nulls=False):
d = super(FormMeta, self).as_dict(no_nulls=no_nulls)

for k in ("__js", "__css", "__list_js", "__calendar_js", "__map_js",
"__linked_with", "__messages", "__print_formats", "__workflow_docs",
"__form_grid_templates", "__listview_template", "__tree_js",
"__dashboard", "__kanban_column_fields", '__templates',
'__custom_js', '__custom_list_js'):
for k in ASSET_KEYS:
d[k] = self.get(k)

# d['fields'] = d.get('fields', [])
@@ -172,7 +183,7 @@ class FormMeta(Meta):
WHERE doc_type=%s AND docstatus<2 and disabled=0""", (self.name,), as_dict=1,
update={"doctype":"Print Format"})

self.set("__print_formats", print_formats, as_value=True)
self.set("__print_formats", print_formats)

def load_workflows(self):
# get active workflow
@@ -186,7 +197,7 @@ class FormMeta(Meta):
for d in workflow.get("states"):
workflow_docs.append(frappe.get_doc("Workflow State", d.state))

self.set("__workflow_docs", workflow_docs, as_value=True)
self.set("__workflow_docs", workflow_docs)


def load_templates(self):
@@ -208,7 +219,7 @@ class FormMeta(Meta):
for content in self.get("__form_grid_templates").values():
messages = extract_messages_from_code(content)
messages = make_dict_from_messages(messages)
self.get("__messages").update(messages, as_value=True)
self.get("__messages").update(messages)

def load_dashboard(self):
self.set('__dashboard', self.get_dashboard_data())
@@ -224,7 +235,7 @@ class FormMeta(Meta):

fields = [x['field_name'] for x in values]
fields = list(set(fields))
self.set("__kanban_column_fields", fields, as_value=True)
self.set("__kanban_column_fields", fields)
except frappe.PermissionError:
# no access to kanban board
pass


+ 1
- 1
frappe/desk/page/setup_wizard/setup_wizard.py Ver arquivo

@@ -392,7 +392,7 @@ def make_records(records, debug=False):
doc.flags.ignore_mandatory = True

try:
doc.insert(ignore_permissions=True)
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
frappe.db.commit()

except frappe.DuplicateEntryError as e:


+ 17
- 7
frappe/desk/query_report.py Ver arquivo

@@ -73,7 +73,7 @@ def get_report_result(report, filters):
return res

@frappe.read_only()
def generate_report_result(report, filters=None, user=None, custom_columns=None):
def generate_report_result(report, filters=None, user=None, custom_columns=None, report_settings=None):
user = user or frappe.session.user
filters = filters or []

@@ -108,7 +108,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
result = get_filtered_data(report.ref_doctype, columns, result, user)

if cint(report.add_total_row) and result and not skip_total_row:
result = add_total_row(result, columns)
result = add_total_row(result, columns, report_settings=report_settings)

return {
"result": result,
@@ -210,7 +210,7 @@ def get_script(report_name):

@frappe.whitelist()
@frappe.read_only()
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None):
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, report_settings=None):
report = get_report_doc(report_name)
if not user:
user = frappe.session.user
@@ -238,7 +238,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False, cust
dn = ""
result = get_prepared_report_result(report, filters, dn, user)
else:
result = generate_report_result(report, filters, user, custom_columns)
result = generate_report_result(report, filters, user, custom_columns, report_settings)

result["add_total_row"] = report.add_total_row and not result.get(
"skip_total_row", False
@@ -435,9 +435,19 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi
return result, column_widths


def add_total_row(result, columns, meta=None):
def add_total_row(result, columns, meta=None, report_settings=None):
total_row = [""] * len(columns)
has_percent = []
is_tree = False
parent_field = ''

if report_settings:
if isinstance(report_settings, (str,)):
report_settings = json.loads(report_settings)

is_tree = report_settings.get('tree')
parent_field = report_settings.get('parent_field')

for i, col in enumerate(columns):
fieldtype, options, fieldname = None, None, None
if isinstance(col, str):
@@ -464,12 +474,12 @@ def add_total_row(result, columns, meta=None):
for row in result:
if i >= len(row):
continue

cell = row.get(fieldname) if isinstance(row, dict) else row[i]
if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt(
cell
):
total_row[i] = flt(total_row[i]) + flt(cell)
if not (is_tree and row.get(parent_field)):
total_row[i] = flt(total_row[i]) + flt(cell)

if fieldtype == "Percent" and i not in has_percent:
has_percent.append(i)


+ 3
- 2
frappe/desk/reportview.py Ver arquivo

@@ -533,7 +533,8 @@ def get_stats(stats, doctype, filters=None):
columns = []

for tag in tags:
if not tag in columns: continue
if tag not in columns:
continue
try:
tag_count = frappe.get_list(doctype,
fields=[tag, "count(*)"],
@@ -612,7 +613,7 @@ def scrub_user_tags(tagcount):
alltags = t.split(',')
for tag in alltags:
if tag:
if not tag in rdict:
if tag not in rdict:
rdict[tag] = 0

rdict[tag] += tagdict[t]


+ 1
- 1
frappe/desk/treeview.py Ver arquivo

@@ -15,7 +15,7 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters):

tree_method = frappe.get_attr(tree_method)

if not tree_method in frappe.whitelisted:
if tree_method not in frappe.whitelisted:
frappe.throw(_("Not Permitted"), frappe.PermissionError)

data = tree_method(doctype, parent, **filters)


+ 1
- 1
frappe/desk/utils.py Ver arquivo

@@ -20,4 +20,4 @@ def validate_route_conflict(doctype, name):
raise frappe.NameError

def slug(name):
return name.lower().replace(' ', '-')
return name.lower().replace(' ', '-')

+ 5
- 3
frappe/email/doctype/email_domain/test_email_domain.py Ver arquivo

@@ -20,11 +20,13 @@ class TestDomain(unittest.TestCase):
mail_domain = frappe.get_doc("Email Domain", "test.com")
mail_account = frappe.get_doc("Email Account", "Test")

# Initially, incoming_port is different in domain and account
self.assertNotEqual(mail_account.incoming_port, mail_domain.incoming_port)
# Ensure a different port
mail_account.incoming_port = int(mail_domain.incoming_port) + 5
mail_account.save()
# Trigger update of accounts using this domain
mail_domain.on_update()
mail_account = frappe.get_doc("Email Account", "Test")

mail_account.reload()
# After update, incoming_port in account should match the domain
self.assertEqual(mail_account.incoming_port, mail_domain.incoming_port)



+ 2
- 3
frappe/installer.py Ver arquivo

@@ -184,7 +184,7 @@ def install_app(name, verbose=False, set_as_patched=True):

def add_to_installed_apps(app_name, rebuild_website=True):
installed_apps = frappe.get_installed_apps()
if not app_name in installed_apps:
if app_name not in installed_apps:
installed_apps.append(app_name)
frappe.db.set_global("installed_apps", json.dumps(installed_apps))
frappe.db.commit()
@@ -529,10 +529,9 @@ def extract_sql_gzip(sql_gz_path):
import subprocess

try:
# dvf - decompress, verbose, force
original_file = sql_gz_path
decompressed_file = original_file.rstrip(".gz")
cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file)
cmd = 'gzip --decompress --force < {0} > {1}'.format(original_file, decompressed_file)
subprocess.check_call(cmd, shell=True)
except Exception:
raise


+ 2
- 2
frappe/integrations/doctype/ldap_settings/ldap_settings.py Ver arquivo

@@ -45,8 +45,8 @@ class LDAPSettings(Document):
title=_("Misconfigured"))

if self.ldap_directory_server.lower() == 'custom':
if not self.ldap_group_member_attribute or not self.ldap_group_mappings_section:
frappe.throw(_("Custom LDAP Directoy Selected, please ensure 'LDAP Group Member attribute' and 'LDAP Group Mappings' are entered"),
if not self.ldap_group_member_attribute or not self.ldap_group_objectclass:
frappe.throw(_("Custom LDAP Directoy Selected, please ensure 'LDAP Group Member attribute' and 'Group Object Class' are entered"),
title=_("Misconfigured"))

else:


+ 130
- 59
frappe/migrate.py Ver arquivo

@@ -1,30 +1,54 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE

import json
import os
import sys
from textwrap import dedent

import frappe
import frappe.translate
import frappe.modules.patch_handler
import frappe.model.sync
from frappe.utils.fixtures import sync_fixtures
from frappe.utils.connections import check_connection
from frappe.utils.dashboard import sync_dashboards
import frappe.modules.patch_handler
import frappe.translate
from frappe.cache_manager import clear_global_cache
from frappe.desk.notifications import clear_notifications
from frappe.website.utils import clear_website_cache
from frappe.core.doctype.language.language import sync_languages
from frappe.modules.utils import sync_customizations
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.search.website_search import build_index_for_all_routes
from frappe.database.schema import add_column
from frappe.desk.notifications import clear_notifications
from frappe.modules.patch_handler import PatchType
from frappe.modules.utils import sync_customizations
from frappe.search.website_search import build_index_for_all_routes
from frappe.utils.connections import check_connection
from frappe.utils.dashboard import sync_dashboards
from frappe.utils.fixtures import sync_fixtures
from frappe.website.utils import clear_website_cache

BENCH_START_MESSAGE = dedent(
"""
Cannot run bench migrate without the services running.
If you are running bench in development mode, make sure that bench is running:

$ bench start

Otherwise, check the server logs and ensure that all the required services are running.
"""
)


def migrate(verbose=True, skip_failing=False, skip_search_index=False):
'''Migrate all apps to the current version, will:
def atomic(method):
def wrapper(*args, **kwargs):
try:
ret = method(*args, **kwargs)
frappe.db.commit()
return ret
except Exception:
frappe.db.rollback()
raise

return wrapper


class SiteMigration:
"""Migrate all apps to the current version, will:
- run before migrate hooks
- run patches
- sync doctypes (schema)
@@ -35,70 +59,117 @@ def migrate(verbose=True, skip_failing=False, skip_search_index=False):
- sync languages
- sync web pages (from /www)
- run after migrate hooks
'''

service_status = check_connection(redis_services=["redis_cache"])
if False in service_status.values():
for service in service_status:
if not service_status.get(service, True):
print("{} service is not running.".format(service))
print("""Cannot run bench migrate without the services running.
If you are running bench in development mode, make sure that bench is running:
"""

$ bench start
def __init__(self, skip_failing: bool = False, skip_search_index: bool = False) -> None:
self.skip_failing = skip_failing
self.skip_search_index = skip_search_index

Otherwise, check the server logs and ensure that all the required services are running.""")
sys.exit(1)
def setUp(self):
"""Complete setup required for site migration
"""
frappe.flags.touched_tables = set()
self.touched_tables_file = frappe.get_site_path("touched_tables.json")
add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
clear_global_cache()

touched_tables_file = frappe.get_site_path('touched_tables.json')
if os.path.exists(touched_tables_file):
os.remove(touched_tables_file)
if os.path.exists(self.touched_tables_file):
os.remove(self.touched_tables_file)

try:
add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
frappe.flags.touched_tables = set()
frappe.flags.in_migrate = True

clear_global_cache()
def tearDown(self):
"""Run operations that should be run post schema updation processes
This should be executed irrespective of outcome
"""
frappe.translate.clear_cache()
clear_website_cache()
clear_notifications()

with open(self.touched_tables_file, "w") as f:
json.dump(list(frappe.flags.touched_tables), f, sort_keys=True, indent=4)

if not self.skip_search_index:
print(f"Building search index for {frappe.local.site}")
build_index_for_all_routes()

frappe.publish_realtime("version-update")
frappe.flags.touched_tables.clear()
frappe.flags.in_migrate = False

@atomic
def pre_schema_updates(self):
"""Executes `before_migrate` hooks
"""
for app in frappe.get_installed_apps():
for fn in frappe.get_hooks('before_migrate', app_name=app):
for fn in frappe.get_hooks("before_migrate", app_name=app):
frappe.get_attr(fn)()

frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.pre_model_sync)
@atomic
def run_schema_updates(self):
"""Run patches as defined in patches.txt, sync schema changes as defined in the {doctype}.json files
"""
frappe.modules.patch_handler.run_all(skip_failing=self.skip_failing, patch_type=PatchType.pre_model_sync)
frappe.model.sync.sync_all()
frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.post_model_sync)
frappe.translate.clear_cache()
frappe.modules.patch_handler.run_all(skip_failing=self.skip_failing, patch_type=PatchType.post_model_sync)

@atomic
def post_schema_updates(self):
"""Execute pending migration tasks post patches execution & schema sync
This includes:
* Sync `Scheduled Job Type` and scheduler events defined in hooks
* Sync fixtures & custom scripts
* Sync in-Desk Module Dashboards
* Sync customizations: Custom Fields, Property Setters, Custom Permissions
* Sync Frappe's internal language master
* Sync Portal Menu Items
* Sync Installed Applications Version History
* Execute `after_migrate` hooks
"""
sync_jobs()
sync_fixtures()
sync_dashboards()
sync_customizations()
sync_languages()

frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu()

# syncs static files
clear_website_cache()

# updating installed applications data
frappe.get_single('Installed Applications').update_versions()
frappe.get_single("Portal Settings").sync_menu()
frappe.get_single("Installed Applications").update_versions()

for app in frappe.get_installed_apps():
for fn in frappe.get_hooks('after_migrate', app_name=app):
for fn in frappe.get_hooks("after_migrate", app_name=app):
frappe.get_attr(fn)()

if not skip_search_index:
# Run this last as it updates the current session
print('Building search index for {}'.format(frappe.local.site))
build_index_for_all_routes()

frappe.db.commit()

clear_notifications()

frappe.publish_realtime("version-update")
frappe.flags.in_migrate = False
finally:
with open(touched_tables_file, 'w') as f:
json.dump(list(frappe.flags.touched_tables), f, sort_keys=True, indent=4)
frappe.flags.touched_tables.clear()
def required_services_running(self) -> bool:
"""Returns True if all required services are running. Returns False and prints
instructions to stdout when required services are not available.
"""
service_status = check_connection(redis_services=["redis_cache"])
are_services_running = all(service_status.values())

if not are_services_running:
for service in service_status:
if not service_status.get(service, True):
print(f"Service {service} is not running.")
print(BENCH_START_MESSAGE)

return are_services_running

def run(self, site: str):
"""Run Migrate operation on site specified. This method initializes
and destroys connections to the site database.
"""
if not self.required_services_running():
raise SystemExit(1)

if site:
frappe.init(site=site)
frappe.connect()

self.setUp()
try:
self.pre_schema_updates()
self.run_schema_updates()
finally:
self.post_schema_updates()
self.tearDown()
frappe.destroy()

+ 8
- 4
frappe/model/base_document.py Ver arquivo

@@ -115,14 +115,18 @@ class BaseDocument(object):
return self

def update_if_missing(self, d):
"""Set default values for fields without existing values"""
if isinstance(d, BaseDocument):
d = d.get_valid_dict()

if "doctype" in d:
self.set("doctype", d.get("doctype"))
for key, value in d.items():
# dont_update_if_missing is a list of fieldnames, for which, you don't want to set default value
if (self.get(key) is None) and (value is not None) and (key not in self.dont_update_if_missing):
if (
value is not None
and self.get(key) is None
# dont_update_if_missing is a list of fieldnames
# for which you don't want to set default value
and key not in self.dont_update_if_missing
):
self.set(key, value)

def get_db_value(self, key):


+ 2
- 2
frappe/model/db_query.py Ver arquivo

@@ -330,7 +330,7 @@ class DatabaseQuery(object):
table_name = table_name[7:]
if not table_name[0]=='`':
table_name = f"`{table_name}`"
if not table_name in self.tables:
if table_name not in self.tables:
self.append_table(table_name)

def append_table(self, table_name):
@@ -428,7 +428,7 @@ class DatabaseQuery(object):
f = get_filter(self.doctype, f, additional_filters_config)

tname = ('`tab' + f.doctype + '`')
if not tname in self.tables:
if tname not in self.tables:
self.append_table(tname)

if 'ifnull(' in f.fieldname:


+ 1
- 1
frappe/model/delete_doc.py Ver arquivo

@@ -115,7 +115,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
# All the linked docs should be checked beforehand
frappe.enqueue('frappe.model.delete_doc.delete_dynamic_links',
doctype=doc.doctype, name=doc.name,
is_async=False if frappe.flags.in_test else True)
now=frappe.flags.in_test)

# clear cache for Document
doc.clear_cache()


+ 1
- 1
frappe/model/rename_doc.py Ver arquivo

@@ -339,7 +339,7 @@ def get_link_fields(doctype: str) -> List[Dict]:
if not frappe.flags.link_fields:
frappe.flags.link_fields = {}

if not doctype in frappe.flags.link_fields:
if doctype not in frappe.flags.link_fields:
link_fields = frappe.db.sql("""\
select parent, fieldname,
(select issingle from tabDocType dt


+ 1
- 1
frappe/model/sync.py Ver arquivo

@@ -117,7 +117,7 @@ def get_doc_files(files, start_path):
if os.path.isdir(os.path.join(doctype_path, docname)):
doc_path = os.path.join(doctype_path, docname, docname) + ".json"
if os.path.exists(doc_path):
if not doc_path in files:
if doc_path not in files:
files.append(doc_path)

return files

+ 3
- 2
frappe/modules/import_file.py Ver arquivo

@@ -115,10 +115,11 @@ def import_file_by_path(path: str,force: bool = False,data_import: bool = False,

if not force or db_modified_timestamp:
try:
stored_hash = frappe.db.get_value(doc["doctype"], doc["name"], "migration_hash")
stored_hash = None
if doc["doctype"] == "DocType":
stored_hash = frappe.db.get_value(doc["doctype"], doc["name"], "migration_hash")
except Exception:
frappe.flags.dt += [doc["doctype"]]
stored_hash = None

# if hash exists and is equal no need to update
if stored_hash and stored_hash == calculated_hash:


+ 3
- 1
frappe/public/js/frappe/form/controls/date.js Ver arquivo

@@ -158,8 +158,10 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
return value;
}
get_df_options() {
let df_options = this.df.options;
if (!df_options) return {};
let options = {};
let df_options = this.df.options || '';
if (typeof df_options === 'string') {
try {
options = JSON.parse(df_options);


+ 3
- 1
frappe/public/js/frappe/form/form.js Ver arquivo

@@ -1511,7 +1511,9 @@ frappe.ui.form.Form = class FrappeForm {
// update child doc
opts.child = locals[opts.child.doctype][opts.child.name];

var std_field_list = ["doctype"].concat(frappe.model.std_fields_list);
var std_field_list = ["doctype"]
.concat(frappe.model.std_fields_list)
.concat(frappe.model.child_table_field_list);
for (var key in r.message) {
if (std_field_list.indexOf(key)===-1) {
opts.child[key] = r.message[key];


+ 1
- 1
frappe/public/js/frappe/form/grid.js Ver arquivo

@@ -746,7 +746,7 @@ export default class Grid {
var df = this.visible_columns[i][0];
var colsize = this.visible_columns[i][1];
if (colsize > 1 && colsize < 11
&& !in_list(frappe.model.std_fields_list, df.fieldname)) {
&& frappe.model.is_non_std_field(df.fieldname)) {

if (passes < 3 && ["Int", "Currency", "Float", "Check", "Percent"].indexOf(df.fieldtype) !== -1) {
// don't increase col size of these fields in first 3 passes


+ 5
- 5
frappe/public/js/frappe/form/grid_row.js Ver arquivo

@@ -340,7 +340,7 @@ export default class GridRow {
</div>
<div class='control-input-wrapper selected-fields'>
</div>
<p class='help-box small text-muted hidden-xs'>
<p class='help-box small text-muted'>
<a class='add-new-fields text-muted'>
+ ${__('Add / Remove Columns')}
</a>
@@ -420,18 +420,18 @@ export default class GridRow {
data-label='${docfield.label}' data-type='${docfield.fieldtype}'>

<div class='row'>
<div class='col-md-1'>
<div class='col-md-1' style='padding-top: 2px'>
<a style='cursor: grabbing;'>${frappe.utils.icon('drag', 'xs')}</a>
</div>
<div class='col-md-7' style='padding-left:0px;'>
<div class='col-md-7' style='padding-left:0px; padding-top:3px'>
${__(docfield.label)}
</div>
<div class='col-md-3' style='padding-left:0px;margin-top:-2px;' title='${__('Columns')}'>
<input class='form-control column-width input-xs text-right'
value='${docfield.columns || cint(d.columns)}'
data-fieldname='${docfield.fieldname}' style='background-color: #ffff; display: inline'>
data-fieldname='${docfield.fieldname}' style='background-color: var(--modal-bg); display: inline'>
</div>
<div class='col-md-1'>
<div class='col-md-1' style='padding-top: 3px'>
<a class='text-muted remove-field' data-fieldname='${docfield.fieldname}'>
<i class='fa fa-trash-o' aria-hidden='true'></i>
</a>


+ 8
- 6
frappe/public/js/frappe/form/layout.js Ver arquivo

@@ -98,7 +98,7 @@ frappe.ui.form.Layout = class Layout {
// remove previous color
this.message.removeClass(this.message_color);
}
this.message_color = (color && ['yellow', 'blue'].includes(color)) ? color : 'blue';
this.message_color = (color && ['yellow', 'blue', 'red'].includes(color)) ? color : 'blue';
if (html) {
if (html.substr(0, 1)!=='<') {
// wrap in a block
@@ -554,19 +554,21 @@ frappe.ui.form.Layout = class Layout {

let has_dep = false;

for (let fkey in this.fields_list) {
let f = this.fields_list[fkey];
f.dependencies_clear = true;
const fields = this.fields_list.concat(this.tabs);

for (let fkey in fields) {
let f = fields[fkey];
if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) {
has_dep = true;
break;
}
}

if (!has_dep) return;

// show / hide based on values
for (let i = this.fields_list.length - 1; i >= 0; i--) {
let f = this.fields_list[i];
for (let i = fields.length - 1; i >= 0; i--) {
let f = fields[i];
f.guardian_has_value = true;
if (f.df.depends_on) {
// evaluate guardian


+ 12
- 13
frappe/public/js/frappe/form/multi_select_dialog.js Ver arquivo

@@ -1,6 +1,6 @@
frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
constructor(opts) {
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label, columns */
Object.assign(this, opts);
this.for_select = this.doctype == "[Select]";
if (!this.for_select) {
@@ -400,23 +400,22 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
return this.results.filter(res => checked_values.includes(res.name));
}

get_datatable_columns() {
if (this.get_query && this.get_query().query && this.columns) return this.columns;

if (Array.isArray(this.setters))
return ["name", ...this.setters.map(df => df.fieldname)];

return ["name", ...Object.keys(this.setters)];
}

make_list_row(result = {}) {
var me = this;
// Make a head row by default (if result not passed)
let head = Object.keys(result).length === 0;

let contents = ``;
let columns = ["name"];

if ($.isArray(this.setters)) {
for (let df of this.setters) {
columns.push(df.fieldname);
}
} else {
columns = columns.concat(Object.keys(this.setters));
}

columns.forEach(function (column) {
this.get_datatable_columns().forEach(function (column) {
contents += `<div class="list-item__content ellipsis">
${
head ? `<span class="ellipsis text-muted" title="${__(frappe.model.unscrub(column))}">${__(frappe.model.unscrub(column))}</span>`
@@ -486,7 +485,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {

get_filters_from_setters() {
let me = this;
let filters = this.get_query ? this.get_query().filters : {} || {};
let filters = (this.get_query ? this.get_query().filters : {}) || {};
let filter_fields = [];

if ($.isArray(this.setters)) {


+ 1
- 1
frappe/public/js/frappe/form/tab.js Ver arquivo

@@ -40,7 +40,7 @@ export default class Tab {
hide = true;
}

hide && this.toggle(false);
this.toggle(!hide);
}

toggle(show) {


+ 24
- 1
frappe/public/js/frappe/list/base_list.js Ver arquivo

@@ -204,6 +204,11 @@ frappe.views.BaseList = class BaseList {
};

if (frappe.boot.desk_settings.view_switcher) {
/* @preserve
for translation, don't remove
__("List View") __("Report View") __("Dashboard View") __("Gantt View"),
__("Kanban View") __("Calendar View") __("Image View") __("Inbox View"),
__("Tree View") __("Map View") */
this.views_menu = this.page.add_custom_button_group(__('{0} View', [this.view_name]),
icon_map[this.view_name] || 'list');
this.views_list = new frappe.views.ListViewSelect({
@@ -465,9 +470,14 @@ frappe.views.BaseList = class BaseList {
}

refresh() {
let args = this.get_call_args();
if (this.no_change(args)) {
// console.log('throttled');
return Promise.resolve();
}
this.freeze(true);
// fetch data from server
return frappe.call(this.get_call_args()).then((r) => {
return frappe.call(args).then((r) => {
// render
this.prepare_data(r);
this.toggle_result_area();
@@ -482,6 +492,19 @@ frappe.views.BaseList = class BaseList {
});
}

no_change(args) {
// returns true if arguments are same for the last 3 seconds
// this helps in throttling if called from various sources
if (this.last_args && JSON.stringify(args) === this.last_args) {
return true;
}
this.last_args = JSON.stringify(args);
setTimeout(() => {
this.last_args = null;
}, 3000);
return false;
}

prepare_data(r) {
let data = r.message || {};



+ 1
- 1
frappe/public/js/frappe/list/list_view.js Ver arquivo

@@ -1483,7 +1483,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return [
filter[1],
"=",
JSON.stringify([filter[2], filter[3]]),
encodeURIComponent(JSON.stringify([filter[2], filter[3]])),
].join("");
})
.join("&");


+ 2
- 2
frappe/public/js/frappe/model/meta.js Ver arquivo

@@ -144,7 +144,7 @@ $.extend(frappe.meta, {

get_doctype_for_field: function(doctype, key) {
var out = null;
if(in_list(frappe.model.std_fields_list, key)) {
if (in_list(frappe.model.std_fields_list, key)) {
// standard
out = doctype;
} else if(frappe.meta.has_field(doctype, key)) {
@@ -152,7 +152,7 @@ $.extend(frappe.meta, {
out = doctype;
} else {
frappe.meta.get_table_fields(doctype).every(function(d) {
if(frappe.meta.has_field(d.options, key)) {
if (frappe.meta.has_field(d.options, key) || in_list(frappe.model.child_table_field_list, key)) {
out = d.options;
return false;
}


+ 3
- 1
frappe/public/js/frappe/model/model.js Ver arquivo

@@ -12,6 +12,8 @@ $.extend(frappe.model, {
std_fields_list: ['name', 'owner', 'creation', 'modified', 'modified_by',
'_user_tags', '_comments', '_assign', '_liked_by', 'docstatus', 'idx'],

child_table_field_list: ['parent', 'parenttype', 'parentfield'],

core_doctypes_list: ['DocType', 'DocField', 'DocPerm', 'User', 'Role', 'Has Role',
'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form',
'Customize Form Field', 'Property Setter', 'Custom Field', 'Client Script'],
@@ -83,7 +85,7 @@ $.extend(frappe.model, {
},

is_non_std_field: function(fieldname) {
return !frappe.model.std_fields_list.includes(fieldname);
return ![...frappe.model.std_fields_list, ...frappe.model.child_table_field_list].includes(fieldname);
},

get_std_field: function(fieldname, ignore=false) {


+ 2
- 2
frappe/public/js/frappe/ui/filters/filter_list.js Ver arquivo

@@ -170,7 +170,7 @@ frappe.ui.FilterGroup = class {
validate_args(doctype, fieldname) {
if (doctype && fieldname
&& !frappe.meta.has_field(doctype, fieldname)
&& !frappe.model.std_fields_list.includes(fieldname)) {
&& frappe.model.is_non_std_field(fieldname)) {

frappe.msgprint({
message: __('Invalid filter: {0}', [fieldname.bold()]),
@@ -293,7 +293,7 @@ frappe.ui.FilterGroup = class {
</div>
</div>
<hr class="divider"></hr>
<div class="filter-action-buttons">
<div class="filter-action-buttons mt-2">
<button class="text-muted add-filter btn btn-xs">
+ ${__('Add a Filter')}
</button>


+ 3
- 2
frappe/public/js/frappe/views/reports/query_report.js Ver arquivo

@@ -578,6 +578,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
args: {
report_name: this.report_name,
filters: filters,
report_settings: this.report_settings
},
callback: resolve,
always: () => this.page.btn_secondary.prop('disabled', false)
@@ -834,7 +835,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
let data = this.data;
let columns = this.columns.filter((col) => !col.hidden);

if (this.raw_data.add_total_row) {
if (this.raw_data.add_total_row && !this.report_settings.tree) {
data = data.slice();
data.splice(-1, 1);
}
@@ -854,7 +855,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
treeView: this.tree_report,
layout: 'fixed',
cellHeight: 33,
showTotalRow: this.raw_data.add_total_row,
showTotalRow: this.raw_data.add_total_row && !this.report_settings.tree,
direction: frappe.utils.is_rtl() ? 'rtl' : 'ltr',
hooks: {
columnTotal: frappe.utils.report_column_total


+ 1
- 1
frappe/public/js/frappe/views/reports/report_view.js Ver arquivo

@@ -651,7 +651,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
&& !df.is_virtual
&& !df.hidden
// not a standard field i.e., owner, modified_by, etc.
&& !frappe.model.std_fields_list.includes(df.fieldname))
&& frappe.model.is_non_std_field(df.fieldname))
return true;
return false;
}


+ 6
- 4
frappe/public/scss/common/color_picker.scss Ver arquivo

@@ -94,7 +94,10 @@

.frappe-control[data-fieldtype='Color'] {
input {
padding-left: 40px;
padding-left: 38px;
}
.control-input {
position: relative;
}
.selected-color {
cursor: pointer;
@@ -103,7 +106,7 @@
border-radius: 5px;
background-color: red;
position: absolute;
top: calc(50% + 1px);
top: 5px;
left: 8px;
content: ' ';
&.no-value {
@@ -113,10 +116,9 @@
}
.like-disabled-input {
.color-value {
padding-left: 25px;
padding-left: 26px;
}
.selected-color {
top: 20%;
cursor: default;
}
}


+ 1
- 1
frappe/public/scss/common/grid.scss Ver arquivo

@@ -192,7 +192,7 @@
margin-left: var(--margin-xs);

button {
height: 27px;
height: 24px;
}
}



+ 5
- 0
frappe/public/scss/common/modal.scss Ver arquivo

@@ -225,6 +225,11 @@ body.modal-open[style^="padding-right"] {
}
}

// modal is xs (for grids)
.modal .hidden-xs {
display: none !important;
}

.dialog-assignment-row {
display: flex;
align-items: center;


+ 2
- 2
frappe/public/scss/desk/frappe_datatable.scss Ver arquivo

@@ -58,7 +58,7 @@
}

.link-btn {
top: 6px;
top: 0px;
}

select {
@@ -77,7 +77,7 @@
padding: 0;
border: var(--dt-focus-border-width) solid #9bccf8;

input {
input[type="text"] {
font-size: inherit;
height: 27px;



+ 1
- 1
frappe/realtime.py Ver arquivo

@@ -65,7 +65,7 @@ def publish_realtime(event=None, message=None, room=None,

if after_commit:
params = [event, message, room]
if not params in frappe.local.realtime_log:
if params not in frappe.local.realtime_log:
frappe.local.realtime_log.append(params)
else:
emit_via_redis(event, message, room)


+ 3
- 3
frappe/test_runner.py Ver arquivo

@@ -282,7 +282,7 @@ def make_test_records(doctype, verbose=0, force=False):
if options == "[Select]":
continue

if not options in frappe.local.test_objects:
if options not in frappe.local.test_objects:
frappe.local.test_objects[options] = []
make_test_records(options, verbose, force)
make_test_records_for_doctype(options, verbose, force)
@@ -389,7 +389,7 @@ def make_test_objects(doctype, test_records=None, verbose=None, reset=False):

try:
d.run_method("before_test_insert")
d.insert()
d.insert(ignore_if_duplicate=True)

if docstatus == 1:
d.submit()
@@ -422,7 +422,7 @@ def add_to_test_record_log(doctype):
'''Add `doctype` to site/.test_log
`.test_log` is a cache of all doctypes for which test records are created'''
test_record_log = get_test_record_log()
if not doctype in test_record_log:
if doctype not in test_record_log:
frappe.flags.test_record_log.append(doctype)
with open(frappe.get_site_path('.test_log'), 'w') as f:
f.write('\n'.join(filter(None, frappe.flags.test_record_log)))


+ 133
- 25
frappe/tests/test_commands.py Ver arquivo

@@ -3,25 +3,37 @@

# imports - standard imports
import gzip
import importlib
import json
import os
import shlex
import shutil
import subprocess
from typing import List
import unittest
from contextlib import contextmanager
from functools import wraps
from glob import glob
from typing import List, Optional
from unittest.case import skipIf
from unittest.mock import patch

# imports - third party imports
import click
from click.testing import CliRunner, Result
from click import Command

# imports - module imports
import frappe
import frappe.commands.site
import frappe.commands.utils
import frappe.recorder
from frappe.installer import add_to_installed_apps, remove_app
from frappe.utils import add_to_date, get_bench_path, get_bench_relative_path, now
from frappe.utils.backups import fetch_latest_backups

# imports - third party imports
import click
_result: Optional[Result] = None
TEST_SITE = "commands-site-O4PN2QKA.test" # added random string tag to avoid collisions
CLI_CONTEXT = frappe._dict(sites=[TEST_SITE])


def clean(value) -> str:
@@ -76,7 +88,61 @@ def exists_in_backup(doctypes: List, file: os.PathLike) -> bool:
return len(missing_doctypes) == 0


@contextmanager
def maintain_locals():
pre_site = frappe.local.site
pre_flags = frappe.local.flags.copy()
pre_db = frappe.local.db

try:
yield
finally:
post_site = getattr(frappe.local, "site", None)
if not post_site or post_site != pre_site:
frappe.init(site=pre_site)
frappe.local.db = pre_db
frappe.local.flags.update(pre_flags)


def pass_test_context(f):
@wraps(f)
def decorated_function(*args, **kwargs):
return f(CLI_CONTEXT, *args, **kwargs)
return decorated_function


@contextmanager
def cli(cmd: Command, args: Optional[List] = None):
with maintain_locals():
global _result

patch_ctx = patch("frappe.commands.pass_context", pass_test_context)
_module = cmd.callback.__module__
_cmd = cmd.callback.__qualname__

__module = importlib.import_module(_module)
patch_ctx.start()
importlib.reload(__module)
click_cmd = getattr(__module, _cmd)

try:
_result = CliRunner().invoke(click_cmd, args=args)
_result.command = str(cmd)
yield _result
finally:
patch_ctx.stop()
__module = importlib.import_module(_module)
importlib.reload(__module)
importlib.invalidate_caches()


class BaseTestCommands(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.setup_test_site()
return super().setUpClass()

@classmethod
def execute(self, command, kwargs=None):
site = {"site": frappe.local.site}
cmd_input = None
@@ -102,16 +168,48 @@ class BaseTestCommands(unittest.TestCase):
self.stderr = clean(self._proc.stderr)
self.returncode = clean(self._proc.returncode)

@classmethod
def setup_test_site(cls):
cmd_config = {
"test_site": TEST_SITE,
"admin_password": frappe.conf.admin_password,
"root_login": frappe.conf.root_login,
"root_password": frappe.conf.root_password,
"db_type": frappe.conf.db_type,
}

if not os.path.exists(
os.path.join(TEST_SITE, "site_config.json")
):
cls.execute(
"bench new-site {test_site} --admin-password {admin_password} --db-type"
" {db_type}",
cmd_config,
)

def _formatMessage(self, msg, standardMsg):
output = super(BaseTestCommands, self)._formatMessage(msg, standardMsg)

if not hasattr(self, "command") and _result:
command = _result.command
stdout = _result.stdout_bytes.decode() if _result.stdout_bytes else None
stderr = _result.stderr_bytes.decode() if _result.stderr_bytes else None
returncode = _result.exit_code
else:
command = self.command
stdout = self.stdout
stderr = self.stderr
returncode = self.returncode

cmd_execution_summary = "\n".join([
"-" * 70,
"Last Command Execution Summary:",
"Command: {}".format(self.command) if self.command else "",
"Standard Output: {}".format(self.stdout) if self.stdout else "",
"Standard Error: {}".format(self.stderr) if self.stderr else "",
"Return Code: {}".format(self.returncode) if self.returncode else "",
"Command: {}".format(command) if command else "",
"Standard Output: {}".format(stdout) if stdout else "",
"Standard Error: {}".format(stderr) if stderr else "",
"Return Code: {}".format(returncode) if returncode else "",
]).strip()

return "{}\n\n{}".format(output, cmd_execution_summary)


@@ -135,6 +233,7 @@ class TestCommands(BaseTestCommands):
self.assertEqual(self.returncode, 0)
self.assertEqual(self.stdout[1:-1], frappe.bold(text="DocType"))

@unittest.skip
def test_restore(self):
# step 0: create a site to run the test on
global_config = {
@@ -143,35 +242,30 @@ class TestCommands(BaseTestCommands):
"root_password": frappe.conf.root_password,
"db_type": frappe.conf.db_type,
}
site_data = {"another_site": f"{frappe.local.site}-restore.test", **global_config}
site_data = {"test_site": TEST_SITE, **global_config}
for key, value in global_config.items():
if value:
self.execute(f"bench set-config {key} {value} -g")
self.execute(
"bench new-site {another_site} --admin-password {admin_password} --db-type"
" {db_type}",
site_data,
)

# test 1: bench restore from full backup
self.execute("bench --site {another_site} backup --ignore-backup-conf", site_data)
self.execute("bench --site {test_site} backup --ignore-backup-conf", site_data)
self.execute(
"bench --site {another_site} execute frappe.utils.backups.fetch_latest_backups",
"bench --site {test_site} execute frappe.utils.backups.fetch_latest_backups",
site_data,
)
site_data.update({"database": json.loads(self.stdout)["database"]})
self.execute("bench --site {another_site} restore {database}", site_data)
self.execute("bench --site {test_site} restore {database}", site_data)

# test 2: restore from partial backup
self.execute("bench --site {another_site} backup --exclude 'ToDo'", site_data)
self.execute("bench --site {test_site} backup --exclude 'ToDo'", site_data)
site_data.update({"kw": "\"{'partial':True}\""})
self.execute(
"bench --site {another_site} execute"
"bench --site {test_site} execute"
" frappe.utils.backups.fetch_latest_backups --kwargs {kw}",
site_data,
)
site_data.update({"database": json.loads(self.stdout)["database"]})
self.execute("bench --site {another_site} restore {database}", site_data)
self.execute("bench --site {test_site} restore {database}", site_data)
self.assertEqual(self.returncode, 1)

def test_partial_restore(self):
@@ -226,7 +320,8 @@ class TestCommands(BaseTestCommands):
def test_list_apps(self):
# test 1: sanity check for command
self.execute("bench --site all list-apps")
self.assertEqual(self.returncode, 0)
self.assertIsNotNone(self.returncode)
self.assertIsInstance(self.stdout or self.stderr, str)

# test 2: bare functionality for single site
self.execute("bench --site {site} list-apps")
@@ -242,14 +337,12 @@ class TestCommands(BaseTestCommands):
self.assertSetEqual(list_apps, installed_apps)

# test 3: parse json format
self.execute("bench --site all list-apps --format json")
self.assertEqual(self.returncode, 0)
self.assertIsInstance(json.loads(self.stdout), dict)

self.execute("bench --site {site} list-apps --format json")
self.assertEqual(self.returncode, 0)
self.assertIsInstance(json.loads(self.stdout), dict)

self.execute("bench --site {site} list-apps -f json")
self.assertEqual(self.returncode, 0)
self.assertIsInstance(json.loads(self.stdout), dict)

def test_show_config(self):
@@ -358,7 +451,7 @@ class TestCommands(BaseTestCommands):
)
def test_bench_drop_site_should_archive_site(self):
# TODO: Make this test postgres compatible
site = 'test_site.localhost'
site = TEST_SITE

self.execute(
f"bench new-site {site} --force --verbose "
@@ -585,3 +678,18 @@ class TestRemoveApp(unittest.TestCase):

# nothing to assert, if this fails rest of the test suite will crumble.
remove_app("frappe", dry_run=True, yes=True, no_backup=True)


class TestSiteMigration(BaseTestCommands):
def test_migrate_cli(self):
with cli(frappe.commands.site.migrate) as result:
self.assertTrue(TEST_SITE in result.stdout)
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.exception, None)


class TestBenchBuild(BaseTestCommands):
def test_build_assets(self):
with cli(frappe.commands.utils.build) as result:
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.exception, None)

+ 4
- 2
frappe/translate.py Ver arquivo

@@ -135,7 +135,7 @@ def get_dict(fortype, name=None):
asset_key = fortype + ":" + (name or "-")
translation_assets = cache.hget("translation_assets", frappe.local.lang, shared=True) or {}

if not asset_key in translation_assets:
if asset_key not in translation_assets:
messages = []
if fortype=="doctype":
messages = get_messages_from_doctype(name)
@@ -576,13 +576,15 @@ def get_server_messages(app):

def get_messages_from_include_files(app_name=None):
"""Returns messages from js files included at time of boot like desk.min.js for desk and web"""
from frappe.utils.jinja_globals import bundled_asset
messages = []
app_include_js = frappe.get_hooks("app_include_js", app_name=app_name) or []
web_include_js = frappe.get_hooks("web_include_js", app_name=app_name) or []
include_js = app_include_js + web_include_js

for js_path in include_js:
relative_path = os.path.join(frappe.local.sites_path, js_path.lstrip('/'))
file_path = bundled_asset(js_path)
relative_path = os.path.join(frappe.local.sites_path, file_path.lstrip('/'))
messages_from_file = get_messages_from_file(relative_path)
messages.extend(messages_from_file)



+ 3
- 4
frappe/translations/de.csv Ver arquivo

@@ -1530,7 +1530,7 @@ Main Section,Hauptbereich,
Make use of longer keyboard patterns,Nutzen Sie mehr Tastaturmuster,
Manage Third Party Apps,Verwalten von Apps von Drittanbietern,
Mandatory Information missing:,Pflichtangaben fehlen:,
Mandatory field: set role for,Pflichtfeld: set Rolle für,
Mandatory field: set role for,Pflichtfeld: Rolle anwenden auf,
Mandatory field: {0},Pflichtfeld: {0},
"Mandatory fields required in table {0}, Row {1}","Pflichtfelder in der Tabelle erforderlich {0}, Reihe {1}",
Mandatory fields required in {0},Für {0} benötigte Pflichtfelder:,
@@ -2268,7 +2268,7 @@ Set Permissions,Festlegen von Berechtigungen,
Set Permissions on Document Types and Roles,Berechtigungen für Dokumenttypen und Rollen setzen,
Set Property After Alert,Setzen Sie die Eigenschaft nach Alert,
Set Quantity,Anzahl festlegen,
Set Role For,Set Rolle für,
Set Role For,Rolle anwenden auf,
Set User Permissions,Nutzer-Berechtigungen setzen,
Set Value,Wert festlegen,
Set custom roles for page and report,Legen Sie benutzerdefinierte Rollen für Seite und Bericht,
@@ -3732,7 +3732,6 @@ Dr,Soll,
Due Date,Fälligkeitsdatum,
Duplicate,Duplizieren,
Edit Profile,Profil bearbeiten,
Email,Email,
End Time,Endzeit,
Enter Value,Wert eingeben,
Entity Type,Entitätstyp,
@@ -4184,7 +4183,7 @@ Phone Number,Telefonnummer,
Linked Documents,Verknüpfte Dokumente,
Account SID,Konto-SID,
Steps,Schritte,
email,Email,
email,E-Mail,
Component,Komponente,
Subtitle,Untertitel,
Global Defaults,Allgemeine Voreinstellungen,


+ 20
- 15
frappe/utils/background_jobs.py Ver arquivo

@@ -1,6 +1,7 @@
import os
import socket
import time
from functools import lru_cache
from uuid import uuid4
from collections import defaultdict
from typing import List
@@ -20,18 +21,22 @@ from frappe.utils.redis_queue import RedisQueue
from frappe.utils.commands import log


common_site_config = frappe.get_file_json("common_site_config.json")
custom_workers_config = common_site_config.get("workers", {})
default_timeout = 300
queue_timeout = {
"default": default_timeout,
"short": default_timeout,
"long": 1500,
**{
worker: config.get("timeout", default_timeout)
for worker, config in custom_workers_config.items()

@lru_cache()
def get_queues_timeout():
common_site_config = frappe.get_conf()
custom_workers_config = common_site_config.get("workers", {})
default_timeout = 300

return {
"default": default_timeout,
"short": default_timeout,
"long": 1500,
**{
worker: config.get("timeout", default_timeout)
for worker, config in custom_workers_config.items()
}
}
}

redis_connection = None

@@ -57,7 +62,7 @@ def enqueue(method, queue='default', timeout=None, event=None,

q = get_queue(queue, is_async=is_async)
if not timeout:
timeout = queue_timeout.get(queue) or 300
timeout = get_queues_timeout().get(queue) or 300
queue_args = {
"site": frappe.local.site,
"user": frappe.session.user,
@@ -204,7 +209,7 @@ def get_jobs(site=None, queue=None, key='method'):

def get_queue_list(queue_list=None, build_queue_name=False):
'''Defines possible queues. Also wraps a given queue in a list after validating.'''
default_queue_list = list(queue_timeout)
default_queue_list = list(get_queues_timeout())
if queue_list:
if isinstance(queue_list, str):
queue_list = [queue_list]
@@ -236,7 +241,7 @@ def get_queue(qtype, is_async=True):

def validate_queue(queue, default_queue_list=None):
if not default_queue_list:
default_queue_list = list(queue_timeout)
default_queue_list = list(get_queues_timeout())

if queue not in default_queue_list:
frappe.throw(_("Queue should be one of {0}").format(', '.join(default_queue_list)))
@@ -296,7 +301,7 @@ def generate_qname(qtype: str) -> str:
def is_queue_accessible(qobj: Queue) -> bool:
"""Checks whether queue is relate to current bench or not.
"""
accessible_queues = [generate_qname(q) for q in list(queue_timeout)]
accessible_queues = [generate_qname(q) for q in list(get_queues_timeout())]
return qobj.name in accessible_queues

def enqueue_test_job():


+ 1
- 1
frappe/utils/install.py Ver arquivo

@@ -90,7 +90,7 @@ def install_basic_docs():

for d in install_docs:
try:
frappe.get_doc(d).insert()
frappe.get_doc(d).insert(ignore_if_duplicate=True)
except frappe.NameError:
pass



+ 0
- 212
frappe/utils/minify.py Ver arquivo

@@ -1,212 +0,0 @@

# This code is original from jsmin by Douglas Crockford, it was translated to
# Python by Baruch Even. The original code had the following copyright and
# license.
#
# /* jsmin.c
# 2007-05-22
#
# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# The Software shall be used for Good, not Evil.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# */

from io import StringIO

def jsmin(js):
ins = StringIO(js)
outs = StringIO()
JavascriptMinify().minify(ins, outs)
str = outs.getvalue()
if len(str) > 0 and str[0] == '\n':
str = str[1:]
return str

def isAlphanum(c):
"""return true if the character is a letter, digit, underscore,
dollar sign, or non-ASCII character.
"""
return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
(c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126));

class UnterminatedComment(Exception):
pass

class UnterminatedStringLiteral(Exception):
pass

class UnterminatedRegularExpression(Exception):
pass

class JavascriptMinify(object):

def _outA(self):
self.outstream.write(self.theA)
def _outB(self):
self.outstream.write(self.theB)

def _get(self):
"""return the next character from stdin. Watch out for lookahead. If
the character is a control character, translate it to a space or
linefeed.
"""
c = self.theLookahead
self.theLookahead = None
if c is None:
c = self.instream.read(1)
if c >= ' ' or c == '\n':
return c
if c == '': # EOF
return '\000'
if c == '\r':
return '\n'
return ' '

def _peek(self):
self.theLookahead = self._get()
return self.theLookahead

def _next(self):
"""get the next character, excluding comments. peek() is used to see
if an unescaped '/' is followed by a '/' or '*'.
"""
c = self._get()
if c == '/' and self.theA != '\\':
p = self._peek()
if p == '/':
c = self._get()
while c > '\n':
c = self._get()
return c
if p == '*':
c = self._get()
while 1:
c = self._get()
if c == '*':
if self._peek() == '/':
self._get()
return ' '
if c == '\000':
raise UnterminatedComment()

return c

def _action(self, action):
"""do something! What you do is determined by the argument:
1 Output A. Copy B to A. Get the next B.
2 Copy B to A. Get the next B. (Delete A).
3 Get the next B. (Delete B).
action treats a string as a single character. Wow!
action recognizes a regular expression if it is preceded by ( or , or =.
"""
if action <= 1:
self._outA()

if action <= 2:
self.theA = self.theB
if self.theA == "'" or self.theA == '"':
while 1:
self._outA()
self.theA = self._get()
if self.theA == self.theB:
break
if self.theA <= '\n':
raise UnterminatedStringLiteral()
if self.theA == '\\':
self._outA()
self.theA = self._get()


if action <= 3:
self.theB = self._next()
if self.theB == '/' and (self.theA == '(' or self.theA == ',' or
self.theA == '=' or self.theA == ':' or
self.theA == '[' or self.theA == '?' or
self.theA == '!' or self.theA == '&' or
self.theA == '|' or self.theA == ';' or
self.theA == '{' or self.theA == '}' or
self.theA == '\n'):
self._outA()
self._outB()
while 1:
self.theA = self._get()
if self.theA == '/':
break
elif self.theA == '\\':
self._outA()
self.theA = self._get()
elif self.theA <= '\n':
raise UnterminatedRegularExpression()
self._outA()
self.theB = self._next()


def _jsmin(self):
"""Copy the input to the output, deleting the characters which are
insignificant to JavaScript. Comments will be removed. Tabs will be
replaced with spaces. Carriage returns will be replaced with linefeeds.
Most spaces and linefeeds will be removed.
"""
self.theA = '\n'
self._action(3)

while self.theA != '\000':
if self.theA == ' ':
if isAlphanum(self.theB):
self._action(1)
else:
self._action(2)
elif self.theA == '\n':
if self.theB in ['{', '[', '(', '+', '-']:
self._action(1)
elif self.theB == ' ':
self._action(3)
else:
if isAlphanum(self.theB):
self._action(1)
else:
self._action(2)
else:
if self.theB == ' ':
if isAlphanum(self.theA):
self._action(1)
else:
self._action(3)
elif self.theB == '\n':
if self.theA in ['}', ']', ')', '+', '-', '"', '\'']:
self._action(1)
else:
if isAlphanum(self.theA):
self._action(1)
else:
self._action(3)
else:
self._action(1)

def minify(self, instream, outstream):
self.instream = instream
self.outstream = outstream
self.theA = '\n'
self.theB = None
self.theLookahead = None

self._jsmin()
self.instream.close()

+ 1
- 1
frappe/utils/pdf.py Ver arquivo

@@ -155,7 +155,7 @@ def read_options_from_html(html):
toggle_visible_pdf(soup)

# use regex instead of soup-parser
for attr in ("margin-top", "margin-bottom", "margin-left", "margin-right", "page-size", "header-spacing", "orientation"):
for attr in ("margin-top", "margin-bottom", "margin-left", "margin-right", "page-size", "header-spacing", "orientation", "page-width", "page-height"):
try:
pattern = re.compile(r"(\.print-format)([\S|\s][^}]*?)(" + str(attr) + r":)(.+)(mm;)")
match = pattern.findall(html)


+ 2
- 2
frappe/utils/redis_wrapper.py Ver arquivo

@@ -154,7 +154,7 @@ class RedisWrapper(redis.Redis):
_name = self.make_key(name, shared=shared)

# set in local
if not _name in frappe.local.cache:
if _name not in frappe.local.cache:
frappe.local.cache[_name] = {}
frappe.local.cache[_name][key] = value

@@ -173,7 +173,7 @@ class RedisWrapper(redis.Redis):

def hget(self, name, key, generator=None, shared=False):
_name = self.make_key(name, shared=shared)
if not _name in frappe.local.cache:
if _name not in frappe.local.cache:
frappe.local.cache[_name] = {}

if not key: return None


+ 1
- 1
frappe/utils/user.py Ver arquivo

@@ -79,7 +79,7 @@ class UserPermissions:
for r in get_valid_perms():
dt = r['parent']

if not dt in self.perm_map:
if dt not in self.perm_map:
self.perm_map[dt] = {}

for k in frappe.permissions.rights:


+ 1
- 1
frappe/website/utils.py Ver arquivo

@@ -226,7 +226,7 @@ def get_full_index(route=None, app=None):

# order as per index if present
for route, children in children_map.items():
if not route in pages:
if route not in pages:
# no parent (?)
continue



Carregando…
Cancelar
Salvar