diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 4ddb9a903a..6bb2953c92 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -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 {0} provided for the field {1} 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') diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index a400d747ae..c94659c3ae 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -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) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 5d8af93549..ed813d7611 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -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() {