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

This commit is contained in:
Gavin D'souza 2022-02-24 11:56:00 +05:30
commit f8b52d8e4f
62 changed files with 606 additions and 581 deletions

View file

@ -13,3 +13,6 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85
# Updating license headers
34460265554242a8d05fb09f049033b1117e1a2b
# Refactor "not a in b" -> "a not in b"
745297a49d516e5e3c4bb3e1b0c4235e7d31165d

View file

@ -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"')

View file

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

View file

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

View file

@ -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');

View file

@ -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');

View file

@ -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');
});
});
});

View file

@ -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 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 subprocess import getoutput
from tempfile import mkdtemp, mktemp
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))

View file

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

View file

@ -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()) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,4 +20,4 @@ def validate_route_conflict(doctype, name):
raise frappe.NameError
def slug(name):
return name.lower().replace(' ', '-')
return name.lower().replace(' ', '-')

View file

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

View file

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

View file

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

View file

@ -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
import frappe.modules.patch_handler
import frappe.translate
from frappe.cache_manager import clear_global_cache
from frappe.core.doctype.language.language import sync_languages
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
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.cache_manager import clear_global_cache
from frappe.desk.notifications import clear_notifications
from frappe.utils.fixtures import sync_fixtures
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.modules.patch_handler import PatchType
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 atomic(method):
def wrapper(*args, **kwargs):
try:
ret = method(*args, **kwargs)
frappe.db.commit()
return ret
except Exception:
frappe.db.rollback()
raise
def migrate(verbose=True, skip_failing=False, skip_search_index=False):
'''Migrate all apps to the current version, will:
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:
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
$ bench start
Otherwise, check the server logs and ensure that all the required services are running.""")
sys.exit(1)
touched_tables_file = frappe.get_site_path('touched_tables.json')
if os.path.exists(touched_tables_file):
os.remove(touched_tables_file)
try:
add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
def setUp(self):
"""Complete setup required for site migration
"""
frappe.flags.touched_tables = set()
frappe.flags.in_migrate = True
self.touched_tables_file = frappe.get_site_path("touched_tables.json")
add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
clear_global_cache()
if os.path.exists(self.touched_tables_file):
os.remove(self.touched_tables_file)
frappe.flags.in_migrate = True
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()
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())
frappe.db.commit()
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)
clear_notifications()
return are_services_running
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 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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,7 +40,7 @@ export default class Tab {
hide = true;
}
hide && this.toggle(false);
this.toggle(!hide);
}
toggle(show) {

View file

@ -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 || {};

View file

@ -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("&");

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -192,7 +192,7 @@
margin-left: var(--margin-xs);
button {
height: 27px;
height: 24px;
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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.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 --format json")
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)

View file

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

View file

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

1 A4 A4
1530 Make use of longer keyboard patterns Nutzen Sie mehr Tastaturmuster
1531 Manage Third Party Apps Verwalten von Apps von Drittanbietern
1532 Mandatory Information missing: Pflichtangaben fehlen:
1533 Mandatory field: set role for Pflichtfeld: set Rolle für Pflichtfeld: Rolle anwenden auf
1534 Mandatory field: {0} Pflichtfeld: {0}
1535 Mandatory fields required in table {0}, Row {1} Pflichtfelder in der Tabelle erforderlich {0}, Reihe {1}
1536 Mandatory fields required in {0} Für {0} benötigte Pflichtfelder:
2268 Set Permissions on Document Types and Roles Berechtigungen für Dokumenttypen und Rollen setzen
2269 Set Property After Alert Setzen Sie die Eigenschaft nach Alert
2270 Set Quantity Anzahl festlegen
2271 Set Role For Set Rolle für Rolle anwenden auf
2272 Set User Permissions Nutzer-Berechtigungen setzen
2273 Set Value Wert festlegen
2274 Set custom roles for page and report Legen Sie benutzerdefinierte Rollen für Seite und Bericht
3732 Due Date Fälligkeitsdatum
3733 Duplicate Duplizieren
3734 Edit Profile Profil bearbeiten
Email Email
3735 End Time Endzeit
3736 Enter Value Wert eingeben
3737 Entity Type Entitätstyp
4183 Linked Documents Verknüpfte Dokumente
4184 Account SID Konto-SID
4185 Steps Schritte
4186 email Email E-Mail
4187 Component Komponente
4188 Subtitle Untertitel
4189 Global Defaults Allgemeine Voreinstellungen

View file

@ -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():

View file

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

View file

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

View file

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

View file

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

2
frappe/utils/user.py Executable file → Normal file
View file

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

View file

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