From ab8b338b9279e64a7d10bddbd13c087e1ae2d65c Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 6 Sep 2019 10:44:51 +0530 Subject: [PATCH 01/53] feat: migrate to google python api --- frappe/contacts/doctype/contact/contact.json | 16 ++- .../google_contacts/google_contacts.json | 42 +++++- .../google_contacts/google_contacts.py | 132 +++++++++++------- 3 files changed, 127 insertions(+), 63 deletions(-) diff --git a/frappe/contacts/doctype/contact/contact.json b/frappe/contacts/doctype/contact/contact.json index f700411e80..fe692a0ff5 100644 --- a/frappe/contacts/doctype/contact/contact.json +++ b/frappe/contacts/doctype/contact/contact.json @@ -31,7 +31,7 @@ "department", "unsubscribed", "column_break_17", - "source", + "pulled_from_google_contacts", "google_contacts_description" ], "fields": [ @@ -165,11 +165,6 @@ "fieldtype": "Check", "label": "Unsubscribed" }, - { - "fieldname": "source", - "fieldtype": "Data", - "label": "Source" - }, { "fieldname": "middle_name", "fieldtype": "Data", @@ -203,12 +198,19 @@ "fieldtype": "Table", "label": "Phone Nos", "options": "Contact Phone" + }, + { + "default": "0", + "fieldname": "pulled_from_google_contacts", + "fieldtype": "Check", + "label": "Pulled from Google Contacts", + "read_only": 1 } ], "icon": "fa fa-user", "idx": 1, "image_field": "image", - "modified": "2019-08-09 10:23:00.486673", + "modified": "2019-09-06 06:43:02.159233", "modified_by": "Administrator", "module": "Contacts", "name": "Contact", diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.json b/frappe/integrations/doctype/google_contacts/google_contacts.json index 42fb9e68c8..746c52e786 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.json +++ b/frappe/integrations/doctype/google_contacts/google_contacts.json @@ -1,4 +1,5 @@ { + "_comments": "[]", "autoname": "format:GC-{email_id}", "creation": "2019-06-14 00:09:39.441961", "doctype": "DocType", @@ -11,7 +12,12 @@ "cb_00", "last_sync_on", "authorization_code", - "refresh_token" + "refresh_token", + "next_sync_token", + "sync", + "pull_from_google_contacts", + "column_break_12", + "push_to_google_contacts" ], "fields": [ { @@ -22,14 +28,12 @@ }, { "fieldname": "authorization_code", - "fieldtype": "Password", - "hidden": 1, + "fieldtype": "Data", "label": "Authorization Code" }, { "fieldname": "refresh_token", - "fieldtype": "Password", - "hidden": 1, + "fieldtype": "Data", "label": "Refresh Token" }, { @@ -62,9 +66,35 @@ "fieldname": "authorize_google_contacts_access", "fieldtype": "Button", "label": "Authorize Google Contacts Access" + }, + { + "fieldname": "next_sync_token", + "fieldtype": "Data", + "label": "Next Sync Token" + }, + { + "fieldname": "sync", + "fieldtype": "Section Break", + "label": "Sync" + }, + { + "default": "0", + "fieldname": "pull_from_google_contacts", + "fieldtype": "Check", + "label": "Pull from Google Contacts" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "push_to_google_contacts", + "fieldtype": "Check", + "label": "Push to Google Contacts" } ], - "modified": "2019-08-23 13:50:52.789503", + "modified": "2019-09-06 17:11:15.530952", "modified_by": "Administrator", "module": "Integrations", "name": "Google Contacts", diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 4955fc9d7f..27d0cb3bd5 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -5,6 +5,9 @@ from __future__ import unicode_literals import frappe import requests +import googleapiclient.discovery +import google.oauth2.credentials + from frappe.model.document import Document from frappe import _ from frappe.utils import get_request_site_address @@ -60,7 +63,7 @@ def authorize_access(g_contact, reauthorize=None): if not google_contact.authorization_code or reauthorize: frappe.cache().hset("google_contacts", "google_contact", google_contact.name) - return google_callback(client_id=google_settings.client_id, redirect_uri=redirect_uri) + return get_authentication_url(client_id=google_settings.client_id, redirect_uri=redirect_uri) else: try: data = { @@ -83,21 +86,42 @@ def authorize_access(g_contact, reauthorize=None): except Exception as e: frappe.throw(e) +def get_authentication_url(client_id=None, redirect_uri=None): + return { + "url": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&prompt=consent&client_id={}&include_granted_scopes=true&scope={}&redirect_uri={}".format(client_id, SCOPES, redirect_uri) + } + @frappe.whitelist() -def google_callback(client_id=None, redirect_uri=None, code=None): +def google_callback(code=None): """ Authorization code is sent to callback as per the API configuration """ - if code is None: - return { - "url": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&prompt=consent&client_id={}&include_granted_scopes=true&scope={}&redirect_uri={}".format(client_id, SCOPES, redirect_uri) - } - else: - google_contact = frappe.cache().hget("google_contacts", "google_contact") - frappe.db.set_value("Google Contacts", google_contact, "authorization_code", code) - frappe.db.commit() + google_contact = frappe.cache().hget("google_contacts", "google_contact") + frappe.db.set_value("Google Contacts", google_contact, "authorization_code", code) + frappe.db.commit() - authorize_access(google_contact) + authorize_access(google_contact) + +def get_google_contacts_object(g_contact): + """ + Returns an object of Google Calendar along with Google Calendar doc. + """ + google_settings = frappe.get_doc("Google Settings") + account = frappe.get_doc("Google Contacts", g_calendar) + + credentials_dict = { + "token": account.get_access_token(), + "refresh_token": account.get_password(fieldname="refresh_token", raise_exception=False), + "token_uri": get_auth_url(), + "client_id": google_settings.client_id, + "client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False), + "scopes": "https://www.googleapis.com/auth/contacts" + } + + credentials = google.oauth2.credentials.Credentials(**credentials_dict) + google_contacts = googleapiclient.discovery.build("people", "v1", credentials=credentials) + + return google_contacts, account @frappe.whitelist() def sync(g_contact=None): @@ -108,58 +132,66 @@ def sync(g_contact=None): google_contacts = frappe.get_list("Google Contacts", filters=filters) - for google_contact in google_contacts: - doc = frappe.get_doc("Google Contacts", google_contact.name) - access_token = doc.get_access_token() + for g in google_contacts: + return sync_contacts_from_google_contacts(g.name) - headers = {"Authorization": "Bearer {}".format(access_token)} +def sync_contacts_from_google_contacts(g_contact, page_length=10): + """ + Syncs Contacts from Google Contacts. + https://developers.google.com/people/api/rest/v1/contactGroups/list + """ + google_contacts, account = get_google_contacts_object(google_contact.name) + results = [] + contacts_updated = 0 + while True: try: - r = requests.get(REQUEST, headers=headers, params=PARAMS) - except Exception as e: - frappe.throw(e) + sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None + contacts = google_contacts.people().connections().list(resourceName='people/me', maxResults=page_length, + syncToken=sync_token).execute() + except HttpError as err: + frappe.throw(_("Google Contacts - Could not sync contacts from Google Contacts, error code {0}.").format(err.resp.status)) - try: - r = r.json() - except Exception as e: - # if request doesn't return json show HTML ask permissions or to identify the error on google side - frappe.throw(e) + for contact in contacts.get("items"): + results.append(contact) - connections = r.get("connections") - contacts_updated = 0 + if not contacts.get("nextPageToken"): + if contacts.get("nextSyncToken"): + frappe.db.set_value("Google Contacts", account.name, "next_sync_token", contacts.get("nextSyncToken")) + frappe.db.commit() + break - frappe.db.set_value("Google Contacts", doc.name, "last_sync_on", frappe.utils.now_datetime()) + frappe.db.set_value("Google Contacts", doc.name, "last_sync_on", frappe.utils.now_datetime()) - if connections: - for idx, connection in enumerate(connections): - frappe.publish_realtime('import_google_contacts', dict(progress=idx+1, total=r.get("totalPeople")), user=frappe.session.user) + for idx, connection in enumerate(results): + frappe.publish_realtime('import_google_contacts', dict(progress=idx+1, total=r.get("totalPeople")), user=frappe.session.user) - for name in connection.get("names"): - if name.get("metadata").get("primary"): - contact = frappe.get_doc({ - "doctype": "Contact", - "salutation": name.get("honorificPrefix") or "", - "first_name": name.get("givenName") or "", - "middle_name": name.get("middleName") or "", - "last_name": name.get("familyName") or "", - "designation": get_indexed_value(connection.get("organizations"), 0, "title"), - "source": "Google Contacts", - "google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name") - }) + for name in connection.get("names"): + if name.get("metadata").get("primary"): + contact = frappe.get_doc({ + "doctype": "Contact", + "salutation": name.get("honorificPrefix") or "", + "first_name": name.get("givenName") or "", + "middle_name": name.get("middleName") or "", + "last_name": name.get("familyName") or "", + "designation": get_indexed_value(connection.get("organizations"), 0, "title"), + "source": "Google Contacts", + "google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name") + }) - for email in connection.get("emailAddresses", []): - contact.add_email(email_id=email.get("value"), is_primary=1 if email.get("primary") else 0) + for email in connection.get("emailAddresses", []): + contact.add_email(email_id=email.get("value"), is_primary=1 if email.get("primary") else 0) - for phone in connection.get("phoneNumbers", []): - contact.add_phone(phone=phone.get("value"), is_primary=1 if phone.get("primary") else 0) + for phone in connection.get("phoneNumbers", []): + contact.add_phone(phone=phone.get("value"), is_primary=1 if phone.get("primary") else 0) - contact.insert(ignore_permissions=True) + contact.insert(ignore_permissions=True) - if g_contact: - return _("{0} Google Contacts synced.").format(contacts_updated) if contacts_updated > 0 else _("No new Google Contacts synced.") + return _("{0} Google Contacts synced.").format(contacts_updated) if contacts_updated > 0 \ + else _("No new Google Contacts synced.") - if g_contact: - return _("No Google Contacts present to sync.") # If no Google Contacts to sync +def insert_contacts_to_google_contacts(doc, method=None): + pass def get_indexed_value(d, index, key): if not d: From 0c86588bf490351ab2eb8459857845a664d887d2 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 6 Sep 2019 10:57:07 +0530 Subject: [PATCH 02/53] fix: variable names --- .../integrations/doctype/google_contacts/google_contacts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 27d0cb3bd5..609ee19728 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -107,7 +107,7 @@ def get_google_contacts_object(g_contact): Returns an object of Google Calendar along with Google Calendar doc. """ google_settings = frappe.get_doc("Google Settings") - account = frappe.get_doc("Google Contacts", g_calendar) + account = frappe.get_doc("Google Contacts", g_contact) credentials_dict = { "token": account.get_access_token(), @@ -140,7 +140,7 @@ def sync_contacts_from_google_contacts(g_contact, page_length=10): Syncs Contacts from Google Contacts. https://developers.google.com/people/api/rest/v1/contactGroups/list """ - google_contacts, account = get_google_contacts_object(google_contact.name) + google_contacts, account = get_google_contacts_object(g_contact) results = [] contacts_updated = 0 From 6de3e9a1bf5232b0f278213abfbde0f5ec6ca076 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 6 Sep 2019 13:56:00 +0530 Subject: [PATCH 03/53] feat: push contacts to google contacts --- frappe/contacts/doctype/contact/contact.json | 52 +++++++-- frappe/contacts/doctype/contact/contact.py | 3 + frappe/hooks.py | 12 ++ .../google_contacts/google_contacts.py | 103 +++++++++++++++--- 4 files changed, 145 insertions(+), 25 deletions(-) diff --git a/frappe/contacts/doctype/contact/contact.json b/frappe/contacts/doctype/contact/contact.json index fe692a0ff5..45f843428f 100644 --- a/frappe/contacts/doctype/contact/contact.json +++ b/frappe/contacts/doctype/contact/contact.json @@ -1,4 +1,5 @@ { + "_comments": "[]", "allow_events_in_timeline": 1, "allow_import": 1, "allow_rename": 1, @@ -21,7 +22,14 @@ "gender", "phone", "image", + "sync_with_google_contacts", "sb_00", + "google_contacts", + "google_contacts_id", + "cb_00", + "pulled_from_google_contacts", + "google_contacts_description", + "sb_01", "email_ids", "phone_nos", "contact_details", @@ -29,10 +37,7 @@ "links", "more_info", "department", - "unsubscribed", - "column_break_17", - "pulled_from_google_contacts", - "google_contacts_description" + "unsubscribed" ], "fields": [ { @@ -155,10 +160,6 @@ "fieldtype": "Data", "label": "Designation" }, - { - "fieldname": "column_break_17", - "fieldtype": "Column Break" - }, { "default": "0", "fieldname": "unsubscribed", @@ -171,15 +172,17 @@ "label": "Middle Name" }, { - "depends_on": "eval:doc.source==\"Google Contacts\"", + "depends_on": "pulled_from_google_contacts", "fieldname": "google_contacts_description", "fieldtype": "Small Text", "label": "Google Contacts Description" }, { + "collapsible": 1, + "depends_on": "eval:doc.sync_with_google_contacts || doc.pulled_from_google_contacts", "fieldname": "sb_00", "fieldtype": "Section Break", - "label": "Contact Details" + "label": "Google Contacts" }, { "fieldname": "email_ids", @@ -205,12 +208,39 @@ "fieldtype": "Check", "label": "Pulled from Google Contacts", "read_only": 1 + }, + { + "default": "0", + "fieldname": "sync_with_google_contacts", + "fieldtype": "Check", + "label": "Sync with Google Contacts" + }, + { + "fieldname": "google_contacts", + "fieldtype": "Link", + "label": "Google Contacts", + "options": "Google Contacts" + }, + { + "fieldname": "cb_00", + "fieldtype": "Column Break" + }, + { + "fieldname": "sb_01", + "fieldtype": "Section Break", + "label": "Contact Details" + }, + { + "fieldname": "google_contacts_id", + "fieldtype": "Data", + "label": "Google Contacts Id", + "read_only": 1 } ], "icon": "fa fa-user", "idx": 1, "image_field": "image", - "modified": "2019-09-06 06:43:02.159233", + "modified": "2019-09-06 20:23:15.088231", "modified_by": "Administrator", "module": "Contacts", "name": "Contact", diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 085452988a..6857d013a1 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -42,6 +42,9 @@ class Contact(Document): if self.email_id and not self.image: self.image = has_gravatar(self.email_id) + if self.sync_with_google_contacts and not self.google_contacts: + frappe.throw(_("Select Google Contacts to which contact should be synced.")) + deduplicate_dynamic_links(self) def set_user(self): diff --git a/frappe/hooks.py b/frappe/hooks.py index eb0d5bbe77..8243283353 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -136,6 +136,18 @@ doc_events = { "on_change": [ "frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points" ], + }, + "Email Group Member": { + "validate": "frappe.email.doctype.email_group.email_group.restrict_email_group" + }, + "Event": { + "after_insert": "frappe.integrations.doctype.google_calendar.google_calendar.insert_event_in_google_calendar", + "on_update": "frappe.integrations.doctype.google_calendar.google_calendar.update_event_in_google_calendar", + "on_trash": "frappe.integrations.doctype.google_calendar.google_calendar.delete_event_from_google_calendar", + }, + "Contact": { + "after_insert": "frappe.integrations.doctype.google_contacts.google_contacts.insert_contacts_to_google_contacts", + "on_update": "frappe.integrations.doctype.google_contacts.google_contacts.update_contacts_to_google_contacts", } } diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 609ee19728..aaae39b264 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -10,12 +10,11 @@ import google.oauth2.credentials from frappe.model.document import Document from frappe import _ +from googleapiclient.errors import HttpError from frappe.utils import get_request_site_address from frappe.integrations.doctype.google_settings.google_settings import get_auth_url SCOPES = "https://www.googleapis.com/auth/contacts" -REQUEST = "https://people.googleapis.com/v1/people/me/connections" -PARAMS = {"personFields": "names,emailAddresses,organizations,phoneNumbers"} class GoogleContacts(Document): @@ -135,24 +134,28 @@ def sync(g_contact=None): for g in google_contacts: return sync_contacts_from_google_contacts(g.name) -def sync_contacts_from_google_contacts(g_contact, page_length=10): +def sync_contacts_from_google_contacts(g_contact): """ Syncs Contacts from Google Contacts. https://developers.google.com/people/api/rest/v1/contactGroups/list """ google_contacts, account = get_google_contacts_object(g_contact) + + if not account.pull_from_google_contacts: + return + results = [] contacts_updated = 0 while True: try: sync_token = account.get_password(fieldname="next_sync_token", raise_exception=False) or None - contacts = google_contacts.people().connections().list(resourceName='people/me', maxResults=page_length, - syncToken=sync_token).execute() + contacts = google_contacts.people().connections().list(resourceName='people/me',syncToken=sync_token, + personFields="names,emailAddresses,organizations,phoneNumbers").execute() except HttpError as err: - frappe.throw(_("Google Contacts - Could not sync contacts from Google Contacts, error code {0}.").format(err.resp.status)) + frappe.throw(_("Google Contacts - Could not sync contacts from Google Contacts {0}, error code {1}.").format(account.name, err.resp.status)) - for contact in contacts.get("items"): + for contact in contacts.get("connections"): results.append(contact) if not contacts.get("nextPageToken"): @@ -161,29 +164,29 @@ def sync_contacts_from_google_contacts(g_contact, page_length=10): frappe.db.commit() break - frappe.db.set_value("Google Contacts", doc.name, "last_sync_on", frappe.utils.now_datetime()) + frappe.db.set_value("Google Contacts", account.name, "last_sync_on", frappe.utils.now_datetime()) for idx, connection in enumerate(results): - frappe.publish_realtime('import_google_contacts', dict(progress=idx+1, total=r.get("totalPeople")), user=frappe.session.user) + frappe.publish_realtime('import_google_contacts', dict(progress=idx+1, total=len(results)), user=frappe.session.user) for name in connection.get("names"): if name.get("metadata").get("primary"): + contacts_updated += 1 contact = frappe.get_doc({ "doctype": "Contact", - "salutation": name.get("honorificPrefix") or "", "first_name": name.get("givenName") or "", "middle_name": name.get("middleName") or "", "last_name": name.get("familyName") or "", "designation": get_indexed_value(connection.get("organizations"), 0, "title"), - "source": "Google Contacts", + "pulled_from_google_contacts": 1, "google_contacts_description": get_indexed_value(connection.get("organizations"), 0, "name") }) for email in connection.get("emailAddresses", []): - contact.add_email(email_id=email.get("value"), is_primary=1 if email.get("primary") else 0) + contact.add_email(email_id=email.get("value"), is_primary=1 if email.get("metadata").get("primary") else 0) for phone in connection.get("phoneNumbers", []): - contact.add_phone(phone=phone.get("value"), is_primary=1 if phone.get("primary") else 0) + contact.add_phone(phone=phone.get("value"), is_primary=1 if phone.get("metadata").get("primary") else 0) contact.insert(ignore_permissions=True) @@ -191,7 +194,79 @@ def sync_contacts_from_google_contacts(g_contact, page_length=10): else _("No new Google Contacts synced.") def insert_contacts_to_google_contacts(doc, method=None): - pass + """ + Syncs Contacts from Google Contacts. + https://developers.google.com/people/api/rest/v1/people/createContact + """ + if not frappe.db.exists("Google Contacts", {"name": doc.google_contacts}) or doc.pulled_from_google_contacts \ + or not doc.sync_with_google_contacts: + return + + google_contacts, account = get_google_contacts_object(doc.google_contacts) + + if not account.push_to_google_contacts: + return + + names = { + "givenName": doc.first_name, + "middleName": doc.middle_name, + "familyName": doc.last_name + } + + phoneNumbers = [{"value": phone_no.phone} for phone_no in doc.phone_nos] + emailAddresses = [{"value": email_id.email_id} for email_id in doc.email_ids] + + try: + contact = google_contacts.people().createContact(parent='people/me', body={"names": [names],"phoneNumbers": phoneNumbers, + "emailAddresses": emailAddresses}).execute() + frappe.db.set_value("Contact", doc.name, "google_contacts_id", contact.get("resourceName")) + except HttpError as err: + frappe.msgprint(_("Google Calendar - Could not insert contact in Google Contacts {0}, error code {1}.").format(account.name, err.resp.status)) + +def update_contacts_to_google_contacts(doc, method=None): + """ + Syncs Contacts from Google Contacts. + https://developers.google.com/people/api/rest/v1/people/updateContact + """ + # Workaround to avoid triggering updation when Event is being inserted since + # creation and modified are same when inserting doc + if not frappe.db.exists("Google Contacts", {"name": doc.google_contacts}) or doc.modified == doc.creation \ + or not doc.sync_with_google_contacts: + return + + if doc.sync_with_google_contacts and not doc.google_contacts_id: + # If sync_with_google_contacts is checked later, then insert the contact rather than updating it. + insert_contacts_to_google_contacts(doc) + return + + google_contacts, account = get_google_contacts_object(doc.google_contacts) + + if not account.push_to_google_contacts: + return + + names = { + "givenName": doc.first_name, + "middleName": doc.middle_name, + "familyName": doc.last_name + } + + phoneNumbers = [{"value": phone_no.phone} for phone_no in doc.phone_nos] + emailAddresses = [{"value": email_id.email_id} for email_id in doc.email_ids] + + try: + contact = google_contacts.people().get(resourceName=doc.google_contacts_id, \ + personFields="names,emailAddresses,organizations,phoneNumbers").execute() + + contact["names"] = [names] + contact["phoneNumbers"] = phoneNumbers + contact["emailAddresses"] = emailAddresses + + google_contacts.people().updateContact(resourceName=doc.google_contacts_id,body={"names":[names], + "phoneNumbers":phoneNumbers,"emailAddresses":emailAddresses}, + updatePersonFields="names,emailAddresses,organizations,phoneNumbers").execute() + frappe.msgprint(_("Contact Synced with Google Contacts.")) + except HttpError as err: + frappe.msgprint(_("Google Contacts - Could not update contact in Google Contacts {0}, error code {1}.").format(account.name, err.resp.status)) def get_indexed_value(d, index, key): if not d: From 124b1918b0e1f4daca8d0f3bd15479ccda55ef5d Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 6 Sep 2019 14:12:31 +0530 Subject: [PATCH 04/53] fix: pass etag parameter for updation --- .../integrations/doctype/google_contacts/google_contacts.json | 1 - frappe/integrations/doctype/google_contacts/google_contacts.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.json b/frappe/integrations/doctype/google_contacts/google_contacts.json index 746c52e786..bf2039ea25 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.json +++ b/frappe/integrations/doctype/google_contacts/google_contacts.json @@ -1,5 +1,4 @@ { - "_comments": "[]", "autoname": "format:GC-{email_id}", "creation": "2019-06-14 00:09:39.441961", "doctype": "DocType", diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index aaae39b264..faf036c594 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -262,7 +262,7 @@ def update_contacts_to_google_contacts(doc, method=None): contact["emailAddresses"] = emailAddresses google_contacts.people().updateContact(resourceName=doc.google_contacts_id,body={"names":[names], - "phoneNumbers":phoneNumbers,"emailAddresses":emailAddresses}, + "phoneNumbers":phoneNumbers,"emailAddresses":emailAddresses,"etag":contact.get("etag")}, updatePersonFields="names,emailAddresses,organizations,phoneNumbers").execute() frappe.msgprint(_("Contact Synced with Google Contacts.")) except HttpError as err: From db331620f214a3d349d7a2bc0ea96d2c649a92e7 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 6 Sep 2019 16:41:42 +0530 Subject: [PATCH 05/53] fix: remove unused hooks --- frappe/hooks.py | 3 --- .../doctype/google_contacts/google_contacts.json | 11 +++++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index 8243283353..d2e8570644 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -137,9 +137,6 @@ doc_events = { "frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points" ], }, - "Email Group Member": { - "validate": "frappe.email.doctype.email_group.email_group.restrict_email_group" - }, "Event": { "after_insert": "frappe.integrations.doctype.google_calendar.google_calendar.insert_event_in_google_calendar", "on_update": "frappe.integrations.doctype.google_calendar.google_calendar.update_event_in_google_calendar", diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.json b/frappe/integrations/doctype/google_contacts/google_contacts.json index bf2039ea25..ef22770b51 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.json +++ b/frappe/integrations/doctype/google_contacts/google_contacts.json @@ -27,12 +27,14 @@ }, { "fieldname": "authorization_code", - "fieldtype": "Data", + "fieldtype": "Password", + "hidden": 1, "label": "Authorization Code" }, { "fieldname": "refresh_token", - "fieldtype": "Data", + "fieldtype": "Password", + "hidden": 1, "label": "Refresh Token" }, { @@ -68,7 +70,8 @@ }, { "fieldname": "next_sync_token", - "fieldtype": "Data", + "fieldtype": "Password", + "hidden": 1, "label": "Next Sync Token" }, { @@ -93,7 +96,7 @@ "label": "Push to Google Contacts" } ], - "modified": "2019-09-06 17:11:15.530952", + "modified": "2019-09-06 20:43:38.124849", "modified_by": "Administrator", "module": "Integrations", "name": "Google Contacts", From e3d044473de0d16788dde4ecd441f7dae7cae601 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 6 Sep 2019 17:14:12 +0530 Subject: [PATCH 06/53] chore: fix reference link for api in docstring --- frappe/integrations/doctype/google_contacts/google_contacts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index faf036c594..b267e09429 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -137,7 +137,7 @@ def sync(g_contact=None): def sync_contacts_from_google_contacts(g_contact): """ Syncs Contacts from Google Contacts. - https://developers.google.com/people/api/rest/v1/contactGroups/list + https://developers.google.com/people/api/rest/v1/people.connections/list """ google_contacts, account = get_google_contacts_object(g_contact) From aef0fb7b893624a09d304e0b3cfd61da92561c21 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Fri, 6 Sep 2019 17:19:25 +0530 Subject: [PATCH 07/53] fix: remove _comments from json --- frappe/contacts/doctype/contact/contact.json | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/contacts/doctype/contact/contact.json b/frappe/contacts/doctype/contact/contact.json index 45f843428f..02f29747b6 100644 --- a/frappe/contacts/doctype/contact/contact.json +++ b/frappe/contacts/doctype/contact/contact.json @@ -1,5 +1,4 @@ { - "_comments": "[]", "allow_events_in_timeline": 1, "allow_import": 1, "allow_rename": 1, From 9c0f678a042a38d2b434b65486b1994ed8e51317 Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 10 Sep 2019 13:45:28 +0530 Subject: [PATCH 08/53] fix: fix legends on charts on user profile page --- frappe/desk/page/user_profile/user_profile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js index 486c64192e..5f8ad0b2c0 100644 --- a/frappe/desk/page/user_profile/user_profile.js +++ b/frappe/desk/page/user_profile/user_profile.js @@ -179,6 +179,7 @@ class UserProfile { labels: chart.labels, datasets: chart.datasets }, + truncateLegends: 1, barOptions: { height: 11, depth: 1 From 62276dde484e98f02a372de174134c344bdfd5d7 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 10 Sep 2019 14:47:09 +0530 Subject: [PATCH 09/53] fix(patch): Skip name column from duplicate index check --- frappe/patches/v12_0/delete_duplicate_indexes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/patches/v12_0/delete_duplicate_indexes.py b/frappe/patches/v12_0/delete_duplicate_indexes.py index 5ec60971cc..7f9e4369a2 100644 --- a/frappe/patches/v12_0/delete_duplicate_indexes.py +++ b/frappe/patches/v12_0/delete_duplicate_indexes.py @@ -19,6 +19,7 @@ def execute(): non_unique FROM information_schema.STATISTICS WHERE table_name=%s + AND column_name!='name' AND non_unique=0 ORDER BY index_name; """, table, as_dict=1) From 2de55ab5c63658e93d53117981ae12bf84b05129 Mon Sep 17 00:00:00 2001 From: Aditya Hase Date: Tue, 10 Sep 2019 15:50:43 +0530 Subject: [PATCH 10/53] fix(backgorund_job): Convert UTC time to local time --- frappe/core/page/background_jobs/background_jobs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py index d488ccd64a..10cb7b97ac 100644 --- a/frappe/core/page/background_jobs/background_jobs.py +++ b/frappe/core/page/background_jobs/background_jobs.py @@ -6,7 +6,7 @@ import frappe from rq import Queue, Worker from frappe.utils.background_jobs import get_redis_conn -from frappe.utils import format_datetime, cint +from frappe.utils import format_datetime, cint, convert_utc_to_user_timezone from frappe.utils.scheduler import is_scheduler_inactive from frappe import _ @@ -30,7 +30,7 @@ def get_info(show_failed=False): 'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \ or str(j.kwargs.get('job_name')), 'status': j.status, 'queue': name, - 'creation': format_datetime(j.created_at), + 'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)), 'color': colors[j.status] }) if j.exc_info: From 9928aa38d7ee42514763ef24fd0c3fd7cc2fd90f Mon Sep 17 00:00:00 2001 From: Aditya Hase Date: Tue, 10 Sep 2019 17:08:35 +0530 Subject: [PATCH 11/53] feat(scheduler): Show popup if scheduler is inactive (#8391) --- frappe/public/js/frappe/desk.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index e6c2328c73..35478834c3 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -130,6 +130,21 @@ frappe.Application = Class.extend({ } } this.link_preview = new frappe.ui.LinkPreview(); + + setInterval(function() { + frappe.call({ + method: 'frappe.core.page.background_jobs.background_jobs.get_scheduler_status', + callback: function(r) { + if (r.message[0] == __("Inactive")) { + frappe.msgprint({ + title: __("Scheduler Inactive"), + indicator: "red", + message: __("Background jobs are not running. Please contact Administrator") + }); + } + } + }); + }, 300000); // check every 5 minutes }, setup_frappe_vue() { From a919c43e47995a16de04faa893a03b5cff61ad74 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 11 Sep 2019 00:57:15 +0530 Subject: [PATCH 12/53] fix: empty comments added --- frappe/public/js/frappe/form/controls/comment.js | 2 +- frappe/public/js/frappe/form/footer/timeline.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/comment.js b/frappe/public/js/frappe/form/controls/comment.js index 44c564ad2c..eba82fd7b2 100644 --- a/frappe/public/js/frappe/form/controls/comment.js +++ b/frappe/public/js/frappe/form/controls/comment.js @@ -60,7 +60,7 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({ update_state() { const value = this.get_value(); - if (strip_html(value)) { + if (strip_html(value).trim() != "") { this.button.removeClass('btn-default').addClass('btn-primary'); } else { this.button.addClass('btn-default').removeClass('btn-primary'); diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index eb9d35398f..ebee467fe5 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -30,7 +30,7 @@ frappe.ui.form.Timeline = class Timeline { render_input: true, only_input: true, on_submit: (val) => { - if(strip_html(val)) { + if(strip_html(val).trim() != "") { this.insert_comment(val, this.comment_area.button); } } From 3b3b90234fdc4689804acffea1dcd3a3be66906b Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 11 Sep 2019 09:35:51 +0530 Subject: [PATCH 13/53] fix: Do not bold text parts unwanted - Complete text finally gets wrapped inside bold --- frappe/public/js/frappe/form/footer/timeline.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index eb9d35398f..52bbfe7864 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -585,21 +585,21 @@ frappe.ui.form.Timeline = class Timeline { var df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname); - if(df && !df.hidden) { + if (df && !df.hidden) { var field_display_status = frappe.perm.get_field_display_status(df, null, me.frm.perm); - if(field_display_status === 'Read' || field_display_status === 'Write') { + if (field_display_status === 'Read' || field_display_status === 'Write') { parts.push(__('{0} from {1} to {2}', [ __(df.label), - (frappe.ellipsis(frappe.utils.html2text(p[1]), 40) || '""').bold(), - (frappe.ellipsis(frappe.utils.html2text(p[2]), 40) || '""').bold() + (frappe.ellipsis(frappe.utils.html2text(p[1]), 40) || '""'), + (frappe.ellipsis(frappe.utils.html2text(p[2]), 40) || '""') ])); } } } return parts.length < 3; }); - if(parts.length) { + if (parts.length) { parts = parts.map(frappe.utils.escape_html); out.push(me.get_version_comment(version, __("changed value of {0}", [parts.join(', ').bold()]))); } From c89b8fc9195a7e3aa9f0c43d56aa7a1669e93a58 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 11 Sep 2019 09:49:57 +0530 Subject: [PATCH 14/53] fix: Improper text bold in timeline.js --- .../public/js/frappe/form/footer/timeline.js | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 52bbfe7864..6459a4b30a 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -562,37 +562,36 @@ frappe.ui.form.Timeline = class Timeline { build_version_comments(docinfo, out) { var me = this; docinfo.versions.forEach(function(version) { - if(!version.data) return; + if (!version.data) return; var data = JSON.parse(version.data); // comment - if(data.comment) { + if (data.comment) { out.push(me.get_version_comment(version, data.comment, data.comment_type)); return; } // value changed in parent - if(data.changed && data.changed.length) { - var parts = []; - data.changed.every(function(p) { - if(p[0]==='docstatus') { - if(p[2]==1) { + if (data.changed && data.changed.length) { + const parts = []; + data.changed.every(function (p) { + if (p[0] === 'docstatus') { + if (p[2] == 1) { out.push(me.get_version_comment(version, __('submitted this document'))); - } else if (p[2]==2) { + } else if (p[2] == 2) { out.push(me.get_version_comment(version, __('cancelled this document'))); } } else { - - var df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname); - + p = p.map(frappe.utils.escape_html); + const df = frappe.meta.get_docfield(me.frm.doctype, p[0], me.frm.docname); if (df && !df.hidden) { - var field_display_status = frappe.perm.get_field_display_status(df, null, + const field_display_status = frappe.perm.get_field_display_status(df, null, me.frm.perm); if (field_display_status === 'Read' || field_display_status === 'Write') { parts.push(__('{0} from {1} to {2}', [ __(df.label), - (frappe.ellipsis(frappe.utils.html2text(p[1]), 40) || '""'), - (frappe.ellipsis(frappe.utils.html2text(p[2]), 40) || '""') + (frappe.ellipsis(frappe.utils.html2text(p[1]), 40) || '""').bold(), + (frappe.ellipsis(frappe.utils.html2text(p[2]), 40) || '""').bold() ])); } } @@ -600,8 +599,7 @@ frappe.ui.form.Timeline = class Timeline { return parts.length < 3; }); if (parts.length) { - parts = parts.map(frappe.utils.escape_html); - out.push(me.get_version_comment(version, __("changed value of {0}", [parts.join(', ').bold()]))); + out.push(me.get_version_comment(version, __('changed value of {0}', [parts.join(', ')]))); } } From e955361610ad25cf2d5dee14f09028d8a61a0a4f Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Wed, 11 Sep 2019 12:15:04 +0530 Subject: [PATCH 15/53] fix: if not db host then localhost --- frappe/database/postgres/setup_db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index 79c7c3a304..01a97178f9 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -17,7 +17,7 @@ def setup_database(force, source_sql, verbose): subprocess_env['PGPASSWORD'] = str(frappe.conf.db_password) # bootstrap db subprocess.check_output([ - 'psql', frappe.conf.db_name, '-h', frappe.conf.db_host, '-U', + 'psql', frappe.conf.db_name, '-h', frappe.conf.db_host or 'localhost', '-U', frappe.conf.db_name, '-f', os.path.join(os.path.dirname(__file__), 'framework_postgres.sql') ], env=subprocess_env) From 6460e14aba5df3105b6182000ad834006a76efc0 Mon Sep 17 00:00:00 2001 From: Sahil Khan Date: Wed, 11 Sep 2019 13:06:37 +0530 Subject: [PATCH 16/53] feat(custom_field): add length option in custom field --- .../doctype/custom_field/custom_field.json | 1259 ++--------------- 1 file changed, 108 insertions(+), 1151 deletions(-) diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index 0f31939eb8..9b60ea2b11 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -1,1432 +1,389 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, "allow_import": 1, - "allow_rename": 0, - "beta": 0, "creation": "2013-01-10 16:34:01", - "custom": 0, "description": "Adds a custom field to a DocType", - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "dt", + "label", + "label_help", + "fieldname", + "insert_after", + "length", + "column_break_6", + "fieldtype", + "precision", + "options", + "fetch_from", + "fetch_if_empty", + "options_help", + "section_break_11", + "collapsible", + "collapsible_depends_on", + "default", + "depends_on", + "description", + "permlevel", + "width", + "columns", + "properties", + "reqd", + "unique", + "read_only", + "ignore_user_permissions", + "hidden", + "print_hide", + "print_hide_if_no_value", + "print_width", + "no_copy", + "allow_on_submit", + "in_list_view", + "in_standard_filter", + "in_global_search", + "bold", + "report_hide", + "search_index", + "ignore_xss_filter", + "translatable" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, "bold": 1, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "dt", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Document", - "length": 0, - "no_copy": 0, "oldfieldname": "dt", "oldfieldtype": "Link", "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, "bold": 1, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "label", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Label", - "length": 0, "no_copy": 1, "oldfieldname": "label", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Data" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "label_help", "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Label Help", - "length": 0, - "no_copy": 0, - "oldfieldtype": "HTML", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "HTML" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "fieldname", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Fieldname", - "length": 0, "no_copy": 1, "oldfieldname": "fieldname", "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", "description": "Select the label after which you want to insert new field.", - "fetch_if_empty": 0, "fieldname": "insert_after", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Insert After", - "length": 0, "no_copy": 1, "oldfieldname": "insert_after", - "oldfieldtype": "Select", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Select" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_6", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, "bold": 1, - "collapsible": 0, - "columns": 0, "default": "Data", - "fetch_if_empty": 0, "fieldname": "fieldtype", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Field Type", - "length": 0, - "no_copy": 0, "oldfieldname": "fieldtype", "oldfieldtype": "Select", "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", "description": "Set non-standard precision for a Float or Currency field", - "fetch_if_empty": 0, "fieldname": "precision", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Precision", - "length": 0, - "no_copy": 0, - "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "options", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Options", - "length": 0, - "no_copy": 0, "oldfieldname": "options", - "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Text" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "fetch_from", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Fetch From", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Fetch From" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.", - "fetch_if_empty": 0, "fieldname": "fetch_if_empty", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Fetch If Empty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Fetch If Empty" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "options_help", "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Options Help", - "length": 0, - "no_copy": 0, - "oldfieldtype": "HTML", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "HTML" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "section_break_11", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "eval:doc.fieldtype==\"Section Break\"", - "fetch_if_empty": 0, "fieldname": "collapsible", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Collapsible", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Collapsible" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.fieldtype==\"Section Break\"", - "fetch_if_empty": 0, "fieldname": "collapsible_depends_on", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Collapsible Depends On", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Collapsible Depends On" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "default", "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Default Value", - "length": 0, - "no_copy": 0, "oldfieldname": "default", - "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Text" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "depends_on", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Depends On", - "length": 255, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "length": 255 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "description", "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Field Description", - "length": 0, - "no_copy": 0, "oldfieldname": "description", "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "300px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "300px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", - "fetch_if_empty": 0, "fieldname": "permlevel", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Permission Level", - "length": 0, - "no_copy": 0, "oldfieldname": "permlevel", - "oldfieldtype": "Int", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Int" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "width", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Width", - "length": 0, - "no_copy": 0, "oldfieldname": "width", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Data" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", - "fetch_if_empty": 0, "fieldname": "columns", "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Columns", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Columns" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "properties", "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, "oldfieldtype": "Column Break", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "50%", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "50%" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "reqd", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Is Mandatory Field", - "length": 0, - "no_copy": 0, "oldfieldname": "reqd", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Check" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "unique", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Unique", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Unique" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "read_only", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Read Only", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Read Only" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "eval:doc.fieldtype===\"Link\"", - "fetch_if_empty": 0, "fieldname": "ignore_user_permissions", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Ignore User Permissions", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Ignore User Permissions" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "hidden", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Hidden", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Hidden" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "print_hide", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Print Hide", - "length": 0, - "no_copy": 0, "oldfieldname": "print_hide", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Check" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", - "fetch_if_empty": 0, "fieldname": "print_hide_if_no_value", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Print Hide If No Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Print Hide If No Value" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "print_width", "fieldtype": "Data", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Print Width", - "length": 0, "no_copy": 1, - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "print_hide": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "no_copy", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "No Copy", - "length": 0, - "no_copy": 0, "oldfieldname": "no_copy", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Check" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "allow_on_submit", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Allow on Submit", - "length": 0, - "no_copy": 0, "oldfieldname": "allow_on_submit", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Check" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "in_list_view", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "In List View", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "In List View" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "in_standard_filter", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "In Standard Filter", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "In Standard Filter" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", - "fetch_if_empty": 0, "fieldname": "in_global_search", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "In Global Search", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "In Global Search" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "bold", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Bold", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Bold" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "report_hide", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Report Hide", - "length": 0, - "no_copy": 0, "oldfieldname": "report_hide", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "oldfieldtype": "Check" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "search_index", "fieldtype": "Check", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Index", - "length": 0, "no_copy": 1, - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "print_hide": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", - "fetch_if_empty": 0, "fieldname": "ignore_xss_filter", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Ignore XSS Filter", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Ignore XSS Filter" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", - "fetch_if_empty": 0, "fieldname": "translatable", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Translatable", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Translatable" + }, + { + "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)", + "fieldname": "length", + "fieldtype": "Int", + "label": "Length" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-glass", "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-03-18 18:03:38.155276", + "modified": "2019-09-11 12:57:19.268934", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Administrator", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, "search_fields": "dt,label,fieldtype,options", - "show_name_in_global_search": 0, + "sort_field": "modified", "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file From de8d338b5a19073859706d725ea880ed4f779e8a Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 11 Sep 2019 14:04:09 +0530 Subject: [PATCH 17/53] fix: Check if duplicate file exists on disk --- frappe/core/doctype/file/file.py | 37 +++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index d334dc5346..390b091298 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -22,7 +22,7 @@ import requests import requests.exceptions import imghdr -from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint +from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint, get_site_path from frappe import _ from frappe import conf from frappe.utils.nestedset import NestedSet @@ -201,14 +201,16 @@ class File(NestedSet): duplicate_file = frappe.db.get_value('File', filters, ['name', 'file_url'], as_dict=1) if duplicate_file: - # if it is attached to a document then throw DuplicateEntryError - if self.attached_to_doctype and self.attached_to_name: - self.duplicate_entry = duplicate_file.name - frappe.throw(_("Same file has already been attached to the record"), - frappe.DuplicateEntryError) - # else just use the url, to avoid uploading a duplicate - else: - self.file_url = duplicate_file.file_url + duplicate_file_doc = frappe.get_cached_doc('File', duplicate_file.name) + if duplicate_file_doc.exists_on_disk(): + # if it is attached to a document then throw DuplicateEntryError + if self.attached_to_doctype and self.attached_to_name: + self.duplicate_entry = duplicate_file.name + frappe.throw(_("Same file has already been attached to the record"), + frappe.DuplicateEntryError) + # else just use the url, to avoid uploading a duplicate + else: + self.file_url = duplicate_file.file_url def validate_file_name(self): if not self.file_name and self.file_url: @@ -332,6 +334,9 @@ class File(NestedSet): data = frappe.db.get_value("File", self.file_data_name, ["file_name", "file_url"], as_dict=True) return data.file_url or data.file_name + def exists_on_disk(self): + exists = os.path.exists(self.get_full_path()) + return exists def upload(self): # get record details @@ -495,19 +500,21 @@ class File(NestedSet): self.content_hash = get_content_hash(self.content) self.content_type = mimetypes.guess_type(self.file_name)[0] - _file = False + duplicate_file = None # check if a file exists with the same content hash and is also in the same folder (public or private) if not ignore_existing_file_check: - _file = frappe.get_value("File", { + duplicate_file = frappe.get_value("File", { "content_hash": self.content_hash, "is_private": self.is_private }, - ["file_url"]) + ["file_url", "name"], as_dict=True) - if _file: - self.file_url = _file - file_exists = True + if duplicate_file: + file_doc = frappe.get_cached_doc('File', duplicate_file.name) + if file_doc.exists_on_disk(): + self.file_url = duplicate_file.file_url + file_exists = True if os.path.exists(encode(get_files_path(self.file_name, is_private=self.is_private))): self.file_name = get_file_name(self.file_name, self.content_hash[-6:]) From a40c0d2b12013537ee5593d2ed9c3f5160ad52a7 Mon Sep 17 00:00:00 2001 From: Sahil Khan Date: Wed, 11 Sep 2019 15:09:57 +0530 Subject: [PATCH 18/53] feat(custom_field): update db on updating custom field --- frappe/custom/doctype/custom_field/custom_field.py | 4 +--- frappe/database/schema.py | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index 356a571adc..21679c5bc7 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -62,9 +62,7 @@ class CustomField(Document): # update the schema if not frappe.db.get_value('DocType', self.dt, 'issingle'): - if (self.fieldname not in frappe.db.get_table_columns(self.dt) - or getattr(self, "_old_fieldtype", None) != self.fieldtype): - frappe.db.updatedb(self.dt) + frappe.db.updatedb(self.dt) def on_trash(self): # delete property setter entries diff --git a/frappe/database/schema.py b/frappe/database/schema.py index d7766d15cd..1663eed95f 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -351,5 +351,3 @@ def add_column(doctype, column_name, fieldtype, precision=None): frappe.db.commit() frappe.db.sql("alter table `tab%s` add column %s %s" % (doctype, column_name, get_definition(fieldtype, precision))) - - From ba96f26f2481faa3480cf1c28a6e03150746a1c6 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 11 Sep 2019 15:46:34 +0530 Subject: [PATCH 19/53] fix: Remove unused import --- frappe/core/doctype/file/file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 390b091298..3c89519faf 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -22,7 +22,7 @@ import requests import requests.exceptions import imghdr -from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint, get_site_path +from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint from frappe import _ from frappe import conf from frappe.utils.nestedset import NestedSet From 01041e105e6b8011c72dc9df695e225bf5ce29f8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 11 Sep 2019 16:59:45 +0530 Subject: [PATCH 20/53] fix: strip checkout page not showing options correctly --- .../pages/integrations/stripe_checkout.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frappe/templates/pages/integrations/stripe_checkout.html b/frappe/templates/pages/integrations/stripe_checkout.html index a4906afc30..5696a66eaa 100644 --- a/frappe/templates/pages/integrations/stripe_checkout.html +++ b/frappe/templates/pages/integrations/stripe_checkout.html @@ -12,15 +12,15 @@ {%- block page_content -%} -
-
+
+
{% if image %} {% endif %}

{{description}}

-
-
+
+
-
+
-
+