Merge pull request #8267 from gavindsouza/doctype-fix

refactor: improvement to doctype API
This commit is contained in:
mergify[bot] 2019-08-27 11:44:50 +00:00 committed by GitHub
commit 2eebc463db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 111 additions and 19 deletions

View file

@ -1,14 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# imports - standard imports
from __future__ import unicode_literals
import re, copy, os, shutil
import json
# imports - third party imports
import six
from six import iteritems
import re, copy, os, subprocess
# imports - module imports
import frappe
import frappe.website.render
from frappe import _
from frappe.utils import now, cint
from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields
from frappe.model.document import Document
@ -19,9 +24,7 @@ from frappe.modules import make_boilerplate, get_doc_path
from frappe.database.schema import validate_column_name, validate_column_length
from frappe.model.docfield import supports_translation
from frappe.modules.import_file import get_file_path
from six import iteritems
import frappe.website.render
import json
class InvalidFieldNameError(frappe.ValidationError): pass
class UniqueFieldnameError(frappe.ValidationError): pass
@ -33,10 +36,12 @@ class NonUniqueError(frappe.ValidationError): pass
class CannotIndexedError(frappe.ValidationError): pass
class CannotCreateStandardDoctypeError(frappe.ValidationError): pass
form_grid_templates = {
"fields": "templates/form_grid/fields.html"
}
class DocType(Document):
def get_feed(self):
return self.name
@ -90,6 +95,7 @@ class DocType(Document):
if self.default_print_format and not self.custom:
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
def set_default_in_list_view(self):
'''Set default in-list-view for first 4 mandatory fields'''
if not [d.fieldname for d in self.fields if d.in_list_view]:
@ -100,12 +106,14 @@ class DocType(Document):
cnt += 1
if cnt == 4: break
def set_default_translatable(self):
'''Ensure that non-translatable never will be translatable'''
for d in self.fields:
if d.translatable and not supports_translation(d.fieldtype):
d.translatable = 0
def check_developer_mode(self):
"""Throw exception if not developer mode or via patch"""
if frappe.flags.in_patch or frappe.flags.in_test:
@ -114,6 +122,7 @@ class DocType(Document):
if not frappe.conf.get("developer_mode") and not self.custom:
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError)
def setup_fields_to_fetch(self):
'''Setup query to update values for newly set fetch values'''
try:
@ -158,18 +167,21 @@ class DocType(Document):
)
)
def update_fields_to_fetch(self):
'''Update fetch values based on queries setup'''
if self.flags.update_fields_to_fetch_queries and not self.issingle:
for query in self.flags.update_fields_to_fetch_queries:
frappe.db.sql(query)
def validate_document_type(self):
if self.document_type=="Transaction":
self.document_type = "Document"
if self.document_type=="Master":
self.document_type = "Setup"
def validate_website(self):
"""Ensure that website generator has field 'route'"""
if self.has_web_view:
@ -180,6 +192,7 @@ class DocType(Document):
# clear website cache
frappe.website.render.clear_cache()
def change_modified_of_parent(self):
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
if frappe.flags.in_import:
@ -189,6 +202,7 @@ class DocType(Document):
for p in parent_list:
frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent))
def scrub_field_names(self):
"""Sluggify fieldnames if not set from Label."""
restricted = ('name','parent','creation','modified','modified_by',
@ -218,6 +232,7 @@ class DocType(Document):
# unique is automatically an index
if d.unique: d.search_index = 0
def validate_series(self, autoname=None, name=None):
"""Validate if `autoname` property is correctly set."""
if not autoname: autoname = self.autoname
@ -254,6 +269,7 @@ class DocType(Document):
if used_in:
frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))
def on_update(self):
"""Update database schema, make controller templates if `custom` is not set and clear cache."""
self.delete_duplicate_custom_fields()
@ -295,6 +311,7 @@ class DocType(Document):
clear_linked_doctype_cache()
def delete_duplicate_custom_fields(self):
if not (frappe.db.table_exists(self.name) and frappe.db.table_exists("Custom Field")):
return
@ -307,6 +324,7 @@ class DocType(Document):
dt = {0} and fieldname in ({1})
'''.format('%s', ', '.join(['%s'] * len(fields))), tuple([self.name] + fields), as_dict=True)
def sync_global_search(self):
'''If global search settings are changed, rebuild search properties for this table'''
global_search_fields_before_update = [d.fieldname for d in
@ -324,6 +342,7 @@ class DocType(Document):
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype',
now=now, doctype=self.name)
def set_base_class_for_controller(self):
'''Updates the controller class to subclass from `WebsiteGenertor`,
if it is a subclass of `Document`'''
@ -350,6 +369,7 @@ class DocType(Document):
if hasattr(module, method):
getattr(module, method)()
def before_rename(self, old, new, merge=False):
"""Throw exception if merge. DocTypes cannot be merged."""
if not self.custom and frappe.session.user != "Administrator":
@ -365,6 +385,7 @@ class DocType(Document):
if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch:
self.rename_files_and_folders(old, new)
def after_rename(self, old, new, merge=False):
"""Change table name using `RENAME TABLE` if table exists. Or update
`doctype` property for Single type."""
@ -375,20 +396,24 @@ class DocType(Document):
else:
frappe.db.sql("rename table `tab%s` to `tab%s`" % (old, new))
def rename_files_and_folders(self, old, new):
# move files
new_path = get_doc_path(self.module, 'doctype', new)
subprocess.check_output(['mv', get_doc_path(self.module, 'doctype', old), new_path])
old_path = get_doc_path(self.module, 'doctype', old)
shutil.move(old_path, new_path)
# rename files
for fname in os.listdir(new_path):
if frappe.scrub(old) in fname:
subprocess.check_output(['mv', os.path.join(new_path, fname),
os.path.join(new_path, fname.replace(frappe.scrub(old), frappe.scrub(new)))])
old_file_name = os.path.join(new_path, fname)
new_file_name = os.path.join(new_path, fname.replace(frappe.scrub(old), frappe.scrub(new)))
shutil.move(old_file_name, new_file_name)
self.rename_inside_controller(new, old, new_path)
frappe.msgprint(_('Renamed files and replaced code in controllers, please check!'))
def rename_inside_controller(self, new, old, new_path):
for fname in ('{}.js', '{}.py', '{}_list.js', '{}_calendar.js', 'test_{}.py', 'test_{}.js'):
fname = os.path.join(new_path, fname.format(frappe.scrub(new)))
@ -396,13 +421,25 @@ class DocType(Document):
with open(fname, 'r') as f:
code = f.read()
with open(fname, 'w') as f:
f.write(code.replace(frappe.scrub(old).replace(' ', ''), frappe.scrub(new).replace(' ', '')))
file_content = code.replace(old, new) # replace str with full str (js controllers)
file_content = file_content.replace(frappe.scrub(old), frappe.scrub(new)) # replace str with _ (py imports)
file_content = file_content.replace(old.replace(' ', ''), new.replace(' ', '')) # replace str (py controllers)
f.write(file_content)
# updating json file with new name
doctype_json_path = os.path.join(new_path, '{}.json'.format(frappe.scrub(new)))
current_data = frappe.get_file_json(doctype_json_path)
current_data['name'] = new
with open(doctype_json_path, 'w') as f:
json.dump(current_data, f, indent=1)
def before_reload(self):
"""Preserve naming series changes in Property Setter."""
if not (self.issingle and self.istable):
self.preserve_naming_series_options_in_property_setter()
def preserve_naming_series_options_in_property_setter(self):
"""Preserve naming_series as property setter if it does not exist"""
naming_series = self.get("fields", {"fieldname": "naming_series"})
@ -422,6 +459,7 @@ class DocType(Document):
if naming_series[0].default:
make_property_setter(self.name, "naming_series", "default", naming_series[0].default, "Text", validate_fields_for_doctype=False)
def before_export(self, docdict):
# remove null and empty fields
def remove_null_fields(o):
@ -466,6 +504,7 @@ class DocType(Document):
except ValueError:
pass
@staticmethod
def prepare_for_import(docdict):
# set order of fields from field_order
@ -488,16 +527,19 @@ class DocType(Document):
if "field_order" in docdict:
del docdict["field_order"]
def export_doc(self):
"""Export to standard folder `[module]/doctype/[name]/[name].json`."""
from frappe.modules.export_file import export_to_files
export_to_files(record_list=[['DocType', self.name]], create_init=True)
def import_doc(self):
"""Import from standard folder `[module]/doctype/[name]/[name].json`."""
from frappe.modules.import_module import import_from_files
import_from_files(record_list=[[self.module, 'doctype', self.name]])
def make_controller_template(self):
"""Make boilerplate controller template."""
make_boilerplate("controller._py", self)
@ -514,6 +556,7 @@ class DocType(Document):
make_boilerplate('templates/controller.html', self.as_dict())
make_boilerplate('templates/controller_row.html', self.as_dict())
def make_amendable(self):
"""If is_submittable is set, add amended_from docfields."""
if self.is_submittable:
@ -529,6 +572,7 @@ class DocType(Document):
"no_copy": 1
})
def make_repeatable(self):
"""If allow_auto_repeat is set, add auto_repeat custom field."""
if self.allow_auto_repeat:
@ -537,12 +581,14 @@ class DocType(Document):
df = dict(fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', options='Auto Repeat', insert_after=insert_after, read_only=1, no_copy=1, print_hide=1)
create_custom_field(self.name, df)
def get_max_idx(self):
"""Returns the highest `idx`"""
max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""",
self.name)
return max_idx and max_idx[0][0] or 0
def validate_name(self, name=None):
if not name:
name = self.name
@ -556,11 +602,13 @@ class DocType(Document):
if not is_a_valid_name:
frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError)
def validate_fields_for_doctype(doctype):
doc = frappe.get_doc("DocType", doctype)
doc.delete_duplicate_custom_fields()
validate_fields(frappe.get_meta(doctype, cached=False))
# this is separate because it is also called via custom field
def validate_fields(meta):
"""Validate doctype fields. Checks
@ -580,21 +628,26 @@ def validate_fields(meta):
14. `unique` cannot be checked if there exist non-unique values.
:param meta: `frappe.model.meta.Meta` object to check."""
def check_illegal_characters(fieldname):
validate_column_name(fieldname)
def check_unique_fieldname(docname, fieldname):
duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)))
if len(duplicates) > 1:
frappe.throw(_("{0}: Fieldname {1} appears multiple times in rows {2}").format(docname, fieldname, ", ".join(duplicates)), UniqueFieldnameError)
def check_fieldname_length(fieldname):
validate_column_length(fieldname)
def check_illegal_mandatory(docname, d):
if (d.fieldtype in no_value_fields) and d.fieldtype not in table_fields and d.reqd:
frappe.throw(_("{0}: Field {1} of type {2} cannot be mandatory").format(docname, d.label, d.fieldtype), IllegalMandatoryError)
def check_link_table_options(docname, d):
if frappe.flags.in_patch: return
if d.fieldtype in ("Link",) + table_fields:
@ -613,23 +666,28 @@ def validate_fields(meta):
# fix case
d.options = options
def check_hidden_and_mandatory(docname, d):
if d.hidden and d.reqd and not d.default:
frappe.throw(_("{0}: Field {1} in row {2} cannot be hidden and mandatory without default").format(docname, d.label, d.idx), HiddenAndMandatoryWithoutDefaultError)
def check_width(d):
if d.fieldtype == "Currency" and cint(d.width) < 100:
frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx))
def check_in_list_view(d):
if d.in_list_view and (d.fieldtype in not_allowed_in_list_view):
frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx))
def check_in_global_search(d):
if d.in_global_search and d.fieldtype in no_value_fields:
frappe.throw(_("'In Global Search' not allowed for type {0} in row {1}")
.format(d.fieldtype, d.idx))
def check_dynamic_link_options(d):
if d.fieldtype=="Dynamic Link":
doctype_pointer = list(filter(lambda df: df.fieldname==d.options, fields))
@ -637,6 +695,7 @@ def validate_fields(meta):
or (doctype_pointer[0].fieldtype=="Link" and doctype_pointer[0].options!="DocType"):
frappe.throw(_("Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'"))
def check_illegal_default(d):
if d.fieldtype == "Check" and not d.default:
d.default = '0'
@ -645,10 +704,12 @@ def validate_fields(meta):
if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")):
frappe.throw(_("Default for {0} must be an option").format(d.fieldname))
def check_precision(d):
if d.fieldtype in ("Currency", "Float", "Percent") and d.precision is not None and not (1 <= cint(d.precision) <= 6):
frappe.throw(_("Precision should be between 1 and 6"))
def check_unique_and_text(docname, d):
if meta.issingle:
d.unique = 0
@ -670,6 +731,7 @@ def validate_fields(meta):
if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"):
frappe.throw(_("{0}:Fieldtype {1} for {2} cannot be indexed").format(docname, d.fieldtype, d.label), CannotIndexedError)
def check_fold(fields):
fold_exists = False
for i, f in enumerate(fields):
@ -684,6 +746,7 @@ def validate_fields(meta):
else:
frappe.throw(_("Fold can not be at the end of the form"))
def check_search_fields(meta, fields):
"""Throw exception if `search_fields` don't contain valid fields."""
if not meta.search_fields:
@ -700,6 +763,7 @@ def validate_fields(meta):
(fieldname not in fieldname_list):
frappe.throw(_("Search field {0} is not valid").format(fieldname))
def check_title_field(meta):
"""Throw exception if `title_field` isn't a valid fieldname."""
if not meta.get("title_field"):
@ -726,6 +790,7 @@ def validate_fields(meta):
_validate_title_field_pattern(df.options)
_validate_title_field_pattern(df.default)
def check_image_field(meta):
'''check image_field exists and is of type "Attach Image"'''
if not meta.image_field:
@ -737,6 +802,7 @@ def validate_fields(meta):
if df[0].fieldtype != 'Attach Image':
frappe.throw(_("Image field must be of type Attach Image"), InvalidFieldNameError)
def check_is_published_field(meta):
if not meta.is_published_field:
return
@ -744,6 +810,7 @@ def validate_fields(meta):
if meta.is_published_field not in fieldname_list:
frappe.throw(_("Is Published Field must be a valid fieldname"), InvalidFieldNameError)
def check_timeline_field(meta):
if not meta.timeline_field:
return
@ -755,6 +822,7 @@ def validate_fields(meta):
if df.fieldtype not in ("Link", "Dynamic Link"):
frappe.throw(_("Timeline field must be a Link or Dynamic Link"), InvalidFieldNameError)
def check_sort_field(meta):
'''Validate that sort_field(s) is a valid field'''
if meta.sort_field:
@ -767,6 +835,7 @@ def validate_fields(meta):
frappe.throw(_("Sort field {0} must be a valid fieldname").format(fieldname),
InvalidFieldNameError)
def check_illegal_depends_on_conditions(docfield):
''' assignment operation should not be allowed in the depends on condition.'''
depends_on_fields = ["depends_on", "collapsible_depends_on"]
@ -776,6 +845,7 @@ def validate_fields(meta):
re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", depends_on):
frappe.throw(_("Invalid {0} condition").format(frappe.unscrub(field)), frappe.ValidationError)
def check_table_multiselect_option(docfield):
'''check if the doctype provided in Option has atleast 1 Link field'''
if not docfield.fieldtype == 'Table MultiSelect': return
@ -788,6 +858,7 @@ def validate_fields(meta):
frappe.throw(_('DocType <b>{0}</b> provided for the field <b>{1}</b> must have atleast one Link field')
.format(doctype, docfield.fieldname), frappe.ValidationError)
def scrub_options_in_select(field):
"""Strip options for whitespaces"""
@ -799,6 +870,7 @@ def validate_fields(meta):
options_list.append(_option)
field.options = '\n'.join(options_list)
def scrub_fetch_from(field):
if hasattr(field, 'fetch_from') and getattr(field, 'fetch_from'):
field.fetch_from = field.fetch_from.strip('\n').strip()
@ -841,6 +913,7 @@ def validate_fields(meta):
check_sort_field(meta)
check_image_field(meta)
def validate_permissions_for_doctype(doctype, for_remove=False):
"""Validates if permissions are set correctly."""
doctype = frappe.get_doc("DocType", doctype)
@ -852,6 +925,7 @@ def validate_permissions_for_doctype(doctype, for_remove=False):
clear_permissions_cache(doctype.name)
def clear_permissions_cache(doctype):
frappe.clear_cache(doctype=doctype)
delete_notification_count_for(doctype)
@ -866,6 +940,7 @@ def clear_permissions_cache(doctype):
""", doctype):
frappe.clear_cache(user=user)
def validate_permissions(doctype, for_remove=False):
permissions = doctype.get("permissions")
if not permissions:
@ -959,6 +1034,7 @@ def validate_permissions(doctype, for_remove=False):
check_level_zero_is_set(d)
remove_rights_for_single(d)
def make_module_and_roles(doc, perm_fieldname="permissions"):
"""Make `Module Def` and `Role` records if already not made. Called while installing."""
try:
@ -989,11 +1065,6 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
else:
raise
def init_list(doctype):
"""Make boilerplate list views."""
doc = frappe.get_meta(doctype)
make_boilerplate("controller_list.js", doc)
make_boilerplate("controller_list.html", doc)
def check_if_fieldname_conflicts_with_methods(doctype, fieldname):
doc = frappe.get_doc({"doctype": doctype})
@ -1002,5 +1073,6 @@ def check_if_fieldname_conflicts_with_methods(doctype, fieldname):
if fieldname in method_list:
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname))
def clear_linked_doctype_cache():
frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled')

View file

@ -2,17 +2,21 @@
# MIT License. See license.txt
from __future__ import unicode_literals
import os
from six import string_types, integer_types
import shutil
import frappe
import frappe.model.meta
from frappe.model.dynamic_links import get_dynamic_link_map
import frappe.defaults
import frappe.model.meta
from frappe import _
from frappe import get_module_path
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.core.doctype.file.file import remove_all
from frappe.utils.password import delete_all_passwords_for
from frappe import _
from frappe.model.naming import revert_series_if_last
from frappe.utils.global_search import delete_for_document
from six import string_types, integer_types
doctypes_to_skip = ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", "File", "Version", "Document Follow", "Comment" , "View Log")
@ -70,6 +74,13 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
delete_from_table(doctype, name, ignore_doctypes, None)
if not (frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_test):
try:
delete_controllers(name, doc.module)
except FileNotFoundError:
# in case a doctype doesnt have any controller code
pass
else:
doc = frappe.get_doc(doctype, name)
@ -323,3 +334,12 @@ def insert_feed(doc):
"subject": "{0} {1}".format(_(doc.doctype), doc.name),
"full_name": get_fullname(doc.owner)
}).insert(ignore_permissions=True)
def delete_controllers(doctype, module):
"""
Delete controller code in the doctype folder
"""
module_path = get_module_path(module)
dir_path = os.path.join(module_path, 'doctype', frappe.scrub(doctype))
shutil.rmtree(dir_path)

View file

@ -839,7 +839,7 @@ frappe.ui.form.Form = class FrappeForm {
}
rename_doc() {
frappe.model.rename_doc(this.doctype, this.docname);
frappe.model.rename_doc(this.doctype, this.docname, () => this.refresh_header());
}
share_doc() {