@@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json | |||||
from .exceptions import * | from .exceptions import * | ||||
from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template | from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template | ||||
__version__ = '9.2.2' | |||||
__version__ = '9.2.3' | |||||
__title__ = "Frappe Framework" | __title__ = "Frappe Framework" | ||||
local = Local() | local = Local() | ||||
@@ -148,7 +148,7 @@ | |||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-10-24 13:25:33.258794", | |||||
"modified": "2017-10-26 09:51:47.663104", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "User Permission", | "name": "User Permission", | ||||
@@ -176,7 +176,7 @@ | |||||
"write": 1 | "write": 1 | ||||
} | } | ||||
], | ], | ||||
"quick_entry": 1, | |||||
"quick_entry": 0, | |||||
"read_only": 0, | "read_only": 0, | ||||
"read_only_onload": 0, | "read_only_onload": 0, | ||||
"show_name_in_global_search": 0, | "show_name_in_global_search": 0, | ||||
@@ -5,19 +5,19 @@ from frappe.utils.password import get_decrypted_password | |||||
class BaseConnection(with_metaclass(ABCMeta)): | class BaseConnection(with_metaclass(ABCMeta)): | ||||
@abstractmethod | @abstractmethod | ||||
def get(self): | |||||
def get(self, remote_objectname, fields=None, filters=None, start=0, page_length=10): | |||||
pass | pass | ||||
@abstractmethod | @abstractmethod | ||||
def insert(self): | |||||
def insert(self, doctype, doc): | |||||
pass | pass | ||||
@abstractmethod | @abstractmethod | ||||
def update(self): | |||||
def update(self, doctype, doc, migration_id): | |||||
pass | pass | ||||
@abstractmethod | @abstractmethod | ||||
def delete(self): | |||||
def delete(self, doctype, migration_id): | |||||
pass | pass | ||||
def get_password(self): | def get_password(self): |
@@ -2,7 +2,46 @@ | |||||
// For license information, please see license.txt | // For license information, please see license.txt | ||||
frappe.ui.form.on('Data Migration Connector', { | frappe.ui.form.on('Data Migration Connector', { | ||||
refresh: function() { | |||||
onload(frm) { | |||||
if(frappe.boot.developer_mode) { | |||||
frm.add_custom_button(__('New Connection'), () => frm.events.new_connection(frm)); | |||||
} | |||||
}, | |||||
new_connection(frm) { | |||||
const d = new frappe.ui.Dialog({ | |||||
title: __('New Connection'), | |||||
fields: [ | |||||
{ label: __('Module'), fieldtype: 'Link', options: 'Module Def', reqd: 1 }, | |||||
{ label: __('Connection Name'), fieldtype: 'Data', description: 'For e.g: Shopify Connection', reqd: 1 }, | |||||
], | |||||
primary_action_label: __('Create'), | |||||
primary_action: (values) => { | |||||
let { module, connection_name } = values; | |||||
frm.events.create_new_connection(module, connection_name) | |||||
.then(r => { | |||||
if (r.message) { | |||||
const connector_name = connection_name | |||||
.replace('connection', 'Connector') | |||||
.replace('Connection', 'Connector') | |||||
.trim(); | |||||
frm.set_value('connector_name', connector_name); | |||||
frm.set_value('connector_type', 'Custom'); | |||||
frm.set_value('python_module', r.message); | |||||
frm.save(); | |||||
frappe.show_alert(__(`New module created ${r.message}`)); | |||||
d.hide(); | |||||
} | |||||
}); | |||||
} | |||||
}); | |||||
d.show(); | |||||
}, | |||||
create_new_connection(module, connection_name) { | |||||
return frappe.call('frappe.data_migration.doctype.data_migration_connector.data_migration_connector.create_new_connection', { | |||||
module, connection_name | |||||
}); | |||||
} | } | ||||
}); | }); |
@@ -49,6 +49,7 @@ | |||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
"columns": 0, | "columns": 0, | ||||
"depends_on": "eval:!doc.is_custom", | |||||
"fieldname": "connector_type", | "fieldname": "connector_type", | ||||
"fieldtype": "Select", | "fieldtype": "Select", | ||||
"hidden": 0, | "hidden": 0, | ||||
@@ -61,7 +62,7 @@ | |||||
"label": "Connector Type", | "label": "Connector Type", | ||||
"length": 0, | "length": 0, | ||||
"no_copy": 0, | "no_copy": 0, | ||||
"options": "Frappe\nPostgres", | |||||
"options": "\nFrappe\nPostgres\nCustom", | |||||
"permlevel": 0, | "permlevel": 0, | ||||
"precision": "", | "precision": "", | ||||
"print_hide": 0, | "print_hide": 0, | ||||
@@ -80,6 +81,7 @@ | |||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
"columns": 0, | "columns": 0, | ||||
"depends_on": "eval:doc.connector_type == 'Custom'", | |||||
"fieldname": "python_module", | "fieldname": "python_module", | ||||
"fieldtype": "Data", | "fieldtype": "Data", | ||||
"hidden": 0, | "hidden": 0, | ||||
@@ -110,7 +112,37 @@ | |||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
"columns": 0, | "columns": 0, | ||||
"default": "localhost", | |||||
"fieldname": "authentication_credentials", | |||||
"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, | |||||
"label": "Authentication Credentials", | |||||
"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, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_bulk_edit": 0, | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"columns": 0, | |||||
"default": "", | |||||
"fieldname": "hostname", | "fieldname": "hostname", | ||||
"fieldtype": "Data", | "fieldtype": "Data", | ||||
"hidden": 0, | "hidden": 0, | ||||
@@ -130,7 +162,7 @@ | |||||
"read_only": 0, | "read_only": 0, | ||||
"remember_last_selected_value": 0, | "remember_last_selected_value": 0, | ||||
"report_hide": 0, | "report_hide": 0, | ||||
"reqd": 1, | |||||
"reqd": 0, | |||||
"search_index": 0, | "search_index": 0, | ||||
"set_only_once": 0, | "set_only_once": 0, | ||||
"unique": 0 | "unique": 0 | ||||
@@ -236,7 +268,7 @@ | |||||
"issingle": 0, | "issingle": 0, | ||||
"istable": 0, | "istable": 0, | ||||
"max_attachments": 0, | "max_attachments": 0, | ||||
"modified": "2017-10-08 14:34:30.603690", | |||||
"modified": "2017-10-26 12:03:40.646348", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Data Migration", | "module": "Data Migration", | ||||
"name": "Data Migration Connector", | "name": "Data Migration Connector", | ||||
@@ -3,9 +3,11 @@ | |||||
# For license information, please see license.txt | # For license information, please see license.txt | ||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | |||||
import frappe, os | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.modules.export_file import create_init_py | |||||
from .connectors.base import BaseConnection | |||||
from .connectors.postgres import PostGresConnection | from .connectors.postgres import PostGresConnection | ||||
from .connectors.frappe_connection import FrappeConnection | from .connectors.frappe_connection import FrappeConnection | ||||
@@ -16,14 +18,14 @@ class DataMigrationConnector(Document): | |||||
if self.python_module: | if self.python_module: | ||||
try: | try: | ||||
frappe.get_module(self.python_module) | |||||
get_connection_class(self.python_module) | |||||
except: | except: | ||||
frappe.throw(frappe._('Invalid module path')) | frappe.throw(frappe._('Invalid module path')) | ||||
def get_connection(self): | def get_connection(self): | ||||
if self.python_module: | if self.python_module: | ||||
module = frappe.get_module(self.python_module) | |||||
return module.get_connection(self) | |||||
_class = get_connection_class(self.python_module) | |||||
return _class(self) | |||||
else: | else: | ||||
if self.connector_type == 'Frappe': | if self.connector_type == 'Frappe': | ||||
self.connection = FrappeConnection(self) | self.connection = FrappeConnection(self) | ||||
@@ -32,8 +34,72 @@ class DataMigrationConnector(Document): | |||||
return self.connection | return self.connection | ||||
def get_objects(self, object_type, condition=None, selection="*"): | |||||
return self.connector.get_objects(object_type, condition, selection) | |||||
@frappe.whitelist() | |||||
def create_new_connection(module, connection_name): | |||||
if not frappe.conf.get('developer_mode'): | |||||
frappe.msgprint(_('Please enable developer mode to create new connection')) | |||||
return | |||||
# create folder | |||||
module_path = frappe.get_module_path(module) | |||||
connectors_folder = os.path.join(module_path, 'connectors') | |||||
frappe.create_folder(connectors_folder) | |||||
def get_join_objects(self, object_type, join_type, primary_key): | |||||
return self.connector.get_join_objects(object_type, join_type, primary_key) | |||||
# create init py | |||||
create_init_py(module_path, 'connectors', '') | |||||
connection_class = connection_name.replace(' ', '') | |||||
file_name = frappe.scrub(connection_name) + '.py' | |||||
file_path = os.path.join(module_path, 'connectors', file_name) | |||||
# create boilerplate file | |||||
with open(file_path, 'w') as f: | |||||
f.write(connection_boilerplate.format(connection_class=connection_class)) | |||||
# get python module string from file_path | |||||
app_name = frappe.db.get_value('Module Def', module, 'app_name') | |||||
python_module = os.path.relpath( | |||||
file_path, '../apps/{0}'.format(app_name)).replace(os.path.sep, '.')[:-3] | |||||
return python_module | |||||
def get_connection_class(python_module): | |||||
filename = python_module.rsplit('.', 1)[-1] | |||||
classname = frappe.unscrub(filename).replace(' ', '') | |||||
module = frappe.get_module(python_module) | |||||
raise_error = False | |||||
if hasattr(module, classname): | |||||
_class = getattr(module, classname) | |||||
if not issubclass(_class, BaseConnection): | |||||
raise_error = True | |||||
else: | |||||
raise_error = True | |||||
if raise_error: | |||||
raise ImportError(filename) | |||||
return _class | |||||
connection_boilerplate = """from __future__ import unicode_literals | |||||
from frappe.data_migration.doctype.data_migration_connector.connectors.base import BaseConnection | |||||
class {connection_class}(BaseConnection): | |||||
def __init__(self, connector): | |||||
# self.connector = connector | |||||
# self.connection = YourModule(self.connector.username, self.get_password()) | |||||
# self.name_field = 'id' | |||||
pass | |||||
def get(self, remote_objectname, fields=None, filters=None, start=0, page_length=10): | |||||
pass | |||||
def insert(self, doctype, doc): | |||||
pass | |||||
def update(self, doctype, doc, migration_id): | |||||
pass | |||||
def delete(self, doctype, migration_id): | |||||
pass | |||||
""" |
@@ -5,6 +5,7 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.utils import get_source_value | |||||
class DataMigrationMapping(Document): | class DataMigrationMapping(Document): | ||||
def get_filters(self): | def get_filters(self): | ||||
@@ -26,6 +27,7 @@ class DataMigrationMapping(Document): | |||||
return fields | return fields | ||||
def get_mapped_record(self, doc): | def get_mapped_record(self, doc): | ||||
'''Build a mapped record using information from the fields table''' | |||||
mapped = frappe._dict() | mapped = frappe._dict() | ||||
key_fieldname = 'remote_fieldname' | key_fieldname = 'remote_fieldname' | ||||
@@ -35,13 +37,19 @@ class DataMigrationMapping(Document): | |||||
key_fieldname, value_fieldname = value_fieldname, key_fieldname | key_fieldname, value_fieldname = value_fieldname, key_fieldname | ||||
for field_map in self.fields: | for field_map in self.fields: | ||||
key = get_source_value(field_map, key_fieldname) | |||||
if not field_map.is_child_table: | if not field_map.is_child_table: | ||||
# field to field mapping | |||||
value = get_value_from_fieldname(field_map, value_fieldname, doc) | value = get_value_from_fieldname(field_map, value_fieldname, doc) | ||||
mapped[field_map.get(key_fieldname)] = value | |||||
else: | else: | ||||
# child table mapping | |||||
mapping_name = field_map.child_table_mapping | mapping_name = field_map.child_table_mapping | ||||
value = get_mapped_child_records(mapping_name, doc.get(field_map.get(value_fieldname))) | |||||
mapped[field_map.get(key_fieldname)] = value | |||||
value = get_mapped_child_records(mapping_name, | |||||
doc.get(get_source_value(field_map, value_fieldname))) | |||||
mapped[key] = value | |||||
return mapped | return mapped | ||||
def get_mapped_child_records(mapping_name, child_docs): | def get_mapped_child_records(mapping_name, child_docs): | ||||
@@ -53,12 +61,12 @@ def get_mapped_child_records(mapping_name, child_docs): | |||||
return mapped_child_docs | return mapped_child_docs | ||||
def get_value_from_fieldname(field_map, fieldname_field, doc): | def get_value_from_fieldname(field_map, fieldname_field, doc): | ||||
field_name = field_map.get(fieldname_field) | |||||
field_name = get_source_value(field_map, fieldname_field) | |||||
if field_name.startswith('eval:'): | if field_name.startswith('eval:'): | ||||
value = frappe.safe_eval(field_name[5:], dict(frappe=frappe)) | value = frappe.safe_eval(field_name[5:], dict(frappe=frappe)) | ||||
elif field_name[0] in ('"', "'"): | elif field_name[0] in ('"', "'"): | ||||
value = field_name[1:-1] | value = field_name[1:-1] | ||||
else: | else: | ||||
value = doc.get(field_name) | |||||
value = get_source_value(doc, field_name) | |||||
return value | return value |
@@ -2,7 +2,9 @@ | |||||
// For license information, please see license.txt | // For license information, please see license.txt | ||||
frappe.ui.form.on('Data Migration Plan', { | frappe.ui.form.on('Data Migration Plan', { | ||||
refresh: function() { | |||||
onload(frm) { | |||||
frm.add_custom_button(__('Run'), () => frappe.new_doc('Data Migration Run', { | |||||
data_migration_plan: frm.doc.name | |||||
})); | |||||
} | } | ||||
}); | }); |
@@ -6,6 +6,7 @@ from __future__ import unicode_literals | |||||
import frappe, json, math | import frappe, json, math | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils import get_source_value | |||||
class DataMigrationRun(Document): | class DataMigrationRun(Document): | ||||
@@ -233,7 +234,8 @@ class DataMigrationRun(Document): | |||||
def get_or_filters(self, mapping): | def get_or_filters(self, mapping): | ||||
or_filters = self.get_last_modified_condition() | or_filters = self.get_last_modified_condition() | ||||
# include docs whose migration_id_field is not set | |||||
# docs whose migration_id_field is not set | |||||
# failed in the previous run, include those too | |||||
or_filters.update({ | or_filters.update({ | ||||
mapping.migration_id_field: ('=', '') | mapping.migration_id_field: ('=', '') | ||||
}) | }) | ||||
@@ -268,9 +270,6 @@ class DataMigrationRun(Document): | |||||
connection = self.get_connection() | connection = self.get_connection() | ||||
data = self.get_new_local_data() | data = self.get_new_local_data() | ||||
push_insert = self.get_log('push_insert', 0) | |||||
push_failed = self.get_log('push_failed', []) | |||||
for d in data: | for d in data: | ||||
# pre process before insert | # pre process before insert | ||||
doc = self.pre_process_doc(d) | doc = self.pre_process_doc(d) | ||||
@@ -282,12 +281,11 @@ class DataMigrationRun(Document): | |||||
mapping.migration_id_field, response_doc[connection.name_field], | mapping.migration_id_field, response_doc[connection.name_field], | ||||
update_modified=False) | update_modified=False) | ||||
frappe.db.commit() | frappe.db.commit() | ||||
self.set_log('push_insert', push_insert + 1) | |||||
self.update_log('push_insert', 1) | |||||
# post process after insert | # post process after insert | ||||
self.post_process_doc(local_doc=d, remote_doc=response_doc) | self.post_process_doc(local_doc=d, remote_doc=response_doc) | ||||
except Exception: | except Exception: | ||||
push_failed.append(d.as_json()) | |||||
self.set_log('push_failed', push_failed) | |||||
self.update_log('push_failed', d.name) | |||||
# update page_start | # update page_start | ||||
self.db_set('current_mapping_start', | self.db_set('current_mapping_start', | ||||
@@ -308,9 +306,6 @@ class DataMigrationRun(Document): | |||||
connection = self.get_connection() | connection = self.get_connection() | ||||
data = self.get_updated_local_data() | data = self.get_updated_local_data() | ||||
push_update = self.get_log('push_update', 0) | |||||
push_failed = self.get_log('push_failed', []) | |||||
for d in data: | for d in data: | ||||
migration_id_value = d.get(mapping.migration_id_field) | migration_id_value = d.get(mapping.migration_id_field) | ||||
# pre process before update | # pre process before update | ||||
@@ -318,12 +313,11 @@ class DataMigrationRun(Document): | |||||
doc = mapping.get_mapped_record(doc) | doc = mapping.get_mapped_record(doc) | ||||
try: | try: | ||||
response_doc = connection.update(mapping.remote_objectname, doc, migration_id_value) | response_doc = connection.update(mapping.remote_objectname, doc, migration_id_value) | ||||
self.set_log('push_update', push_update + 1) | |||||
self.update_log('push_update', 1) | |||||
# post process after update | # post process after update | ||||
self.post_process_doc(local_doc=d, remote_doc=response_doc) | self.post_process_doc(local_doc=d, remote_doc=response_doc) | ||||
except Exception: | except Exception: | ||||
push_failed.append(d.as_json()) | |||||
self.set_log('push_failed', push_failed) | |||||
self.update_log('push_failed', d.name) | |||||
# update page_start | # update page_start | ||||
self.db_set('current_mapping_start', | self.db_set('current_mapping_start', | ||||
@@ -344,9 +338,6 @@ class DataMigrationRun(Document): | |||||
connection = self.get_connection() | connection = self.get_connection() | ||||
data = self.get_deleted_local_data() | data = self.get_deleted_local_data() | ||||
push_delete = self.get_log('push_delete', 0) | |||||
push_failed = self.get_log('push_failed', []) | |||||
for d in data: | for d in data: | ||||
# Deleted Document also has a custom field for migration_id | # Deleted Document also has a custom field for migration_id | ||||
migration_id_value = d.get(mapping.migration_id_field) | migration_id_value = d.get(mapping.migration_id_field) | ||||
@@ -354,12 +345,11 @@ class DataMigrationRun(Document): | |||||
self.pre_process_doc(d) | self.pre_process_doc(d) | ||||
try: | try: | ||||
response_doc = connection.delete(mapping.remote_objectname, migration_id_value) | response_doc = connection.delete(mapping.remote_objectname, migration_id_value) | ||||
self.set_log('push_delete', push_delete + 1) | |||||
self.update_log('push_delete', 1) | |||||
# post process only when action is success | # post process only when action is success | ||||
self.post_process_doc(local_doc=d, remote_doc=response_doc) | self.post_process_doc(local_doc=d, remote_doc=response_doc) | ||||
except Exception: | except Exception: | ||||
push_failed.append(d.as_json()) | |||||
self.set_log('push_failed', push_failed) | |||||
self.update_log('push_failed', d.name) | |||||
# update page_start | # update page_start | ||||
self.db_set('current_mapping_start', | self.db_set('current_mapping_start', | ||||
@@ -377,46 +367,32 @@ class DataMigrationRun(Document): | |||||
mapping = self.get_mapping(self.current_mapping) | mapping = self.get_mapping(self.current_mapping) | ||||
data = self.get_remote_data() | data = self.get_remote_data() | ||||
pull_insert = self.get_log('pull_insert', 0) | |||||
pull_update = self.get_log('pull_update', 0) | |||||
pull_failed = self.get_log('pull_failed', []) | |||||
def get_migration_id_value(source, key): | |||||
value = None | |||||
try: | |||||
value = source[key] | |||||
except: | |||||
value = getattr(source, key) | |||||
return value | |||||
for d in data: | for d in data: | ||||
migration_id_value = get_migration_id_value(d, connection.name_field) | |||||
migration_id_value = get_source_value(d, connection.name_field) | |||||
doc = self.pre_process_doc(d) | doc = self.pre_process_doc(d) | ||||
doc = mapping.get_mapped_record(doc) | doc = mapping.get_mapped_record(doc) | ||||
if migration_id_value: | if migration_id_value: | ||||
if not local_doc_exists(mapping, migration_id_value): | |||||
# insert new local doc | |||||
local_doc = insert_local_doc(mapping, doc) | |||||
self.set_log('pull_insert', pull_insert + 1) | |||||
# set migration id | |||||
frappe.db.set_value(mapping.local_doctype, local_doc.name, | |||||
mapping.migration_id_field, migration_id_value, | |||||
update_modified=False) | |||||
frappe.db.commit() | |||||
else: | |||||
# update doc | |||||
local_doc = update_local_doc(mapping, doc, migration_id_value) | |||||
self.set_log('pull_update', pull_update + 1) | |||||
if local_doc: | |||||
# post process doc after success | |||||
self.post_process_doc(remote_doc=d, local_doc=local_doc) | |||||
else: | |||||
# failed, append to log | |||||
pull_failed.append(d) | |||||
self.set_log('pull_failed', pull_failed) | |||||
try: | |||||
if not local_doc_exists(mapping, migration_id_value): | |||||
# insert new local doc | |||||
local_doc = insert_local_doc(mapping, doc) | |||||
self.update_log('pull_insert', 1) | |||||
# set migration id | |||||
frappe.db.set_value(mapping.local_doctype, local_doc.name, | |||||
mapping.migration_id_field, migration_id_value, | |||||
update_modified=False) | |||||
frappe.db.commit() | |||||
else: | |||||
# update doc | |||||
local_doc = update_local_doc(mapping, doc, migration_id_value) | |||||
self.update_log('pull_update', 1) | |||||
# post process doc after success | |||||
self.post_process_doc(remote_doc=d, local_doc=local_doc) | |||||
except Exception: | |||||
# failed, append to log | |||||
self.update_log('pull_failed', migration_id_value) | |||||
if len(data) < mapping.page_length: | if len(data) < mapping.page_length: | ||||
# last page, done with pull | # last page, done with pull | ||||
@@ -436,6 +412,19 @@ class DataMigrationRun(Document): | |||||
value = json.dumps(value) if '_failed' in key else value | value = json.dumps(value) if '_failed' in key else value | ||||
self.db_set(key, value) | self.db_set(key, value) | ||||
def update_log(self, key, value=None): | |||||
''' | |||||
Helper for updating logs, | |||||
push_failed and pull_failed are stored as json, | |||||
other keys are stored as int | |||||
''' | |||||
if '_failed' in key: | |||||
# json | |||||
self.set_log(key, self.get_log(key, []) + [value]) | |||||
else: | |||||
# int | |||||
self.set_log(key, self.get_log(key, 0) + (value or 1)) | |||||
def get_log(self, key, default=None): | def get_log(self, key, default=None): | ||||
value = self.db_get(key) | value = self.db_get(key) | ||||
if '_failed' in key: | if '_failed' in key: | ||||
@@ -8,13 +8,13 @@ class TestDataMigrationRun(unittest.TestCase): | |||||
def test_run(self): | def test_run(self): | ||||
create_plan() | create_plan() | ||||
description = 'Data migration todo' | |||||
description = 'data migration todo' | |||||
new_todo = frappe.get_doc({ | new_todo = frappe.get_doc({ | ||||
'doctype': 'ToDo', | 'doctype': 'ToDo', | ||||
'description': description | 'description': description | ||||
}).insert() | }).insert() | ||||
event_subject = 'Data migration event' | |||||
event_subject = 'data migration event' | |||||
frappe.get_doc(dict( | frappe.get_doc(dict( | ||||
doctype='Event', | doctype='Event', | ||||
subject=event_subject, | subject=event_subject, | ||||
@@ -62,7 +62,6 @@ class TestDataMigrationRun(unittest.TestCase): | |||||
# Update | # Update | ||||
self.assertEqual(run.db_get('status'), 'Success') | self.assertEqual(run.db_get('status'), 'Success') | ||||
self.assertEqual(run.db_get('push_update'), 1) | |||||
self.assertEqual(run.db_get('pull_update'), 1) | self.assertEqual(run.db_get('pull_update'), 1) | ||||
def create_plan(): | def create_plan(): | ||||
@@ -76,7 +75,8 @@ def create_plan(): | |||||
'fields': [ | 'fields': [ | ||||
{ 'remote_fieldname': 'subject', 'local_fieldname': 'description' }, | { 'remote_fieldname': 'subject', 'local_fieldname': 'description' }, | ||||
{ 'remote_fieldname': 'starts_on', 'local_fieldname': 'eval:frappe.utils.get_datetime_str(frappe.utils.get_datetime())' } | { 'remote_fieldname': 'starts_on', 'local_fieldname': 'eval:frappe.utils.get_datetime_str(frappe.utils.get_datetime())' } | ||||
] | |||||
], | |||||
'condition': '{"description": "data migration todo" }' | |||||
}).insert() | }).insert() | ||||
frappe.get_doc({ | frappe.get_doc({ | ||||
@@ -87,7 +87,7 @@ def create_plan(): | |||||
'local_doctype': 'ToDo', | 'local_doctype': 'ToDo', | ||||
'local_primary_key': 'name', | 'local_primary_key': 'name', | ||||
'mapping_type': 'Pull', | 'mapping_type': 'Pull', | ||||
'condition': '{"subject": "Data migration event"}', | |||||
'condition': '{"subject": "data migration event" }', | |||||
'fields': [ | 'fields': [ | ||||
{ 'remote_fieldname': 'subject', 'local_fieldname': 'description' } | { 'remote_fieldname': 'subject', 'local_fieldname': 'description' } | ||||
] | ] | ||||
@@ -423,7 +423,6 @@ def get_filecontent_from_path(path): | |||||
return filecontent | return filecontent | ||||
else: | else: | ||||
print(full_path + ' doesn\'t exists') | |||||
return None | return None | ||||
@@ -530,6 +530,10 @@ class BaseDocument(object): | |||||
if frappe.flags.in_install: | if frappe.flags.in_install: | ||||
return | return | ||||
if self.meta.issingle: | |||||
# single doctype value type is mediumtext | |||||
return | |||||
for fieldname, value in iteritems(self.get_valid_dict()): | for fieldname, value in iteritems(self.get_valid_dict()): | ||||
df = self.meta.get_field(fieldname) | df = self.meta.get_field(fieldname) | ||||
if df and df.fieldtype in type_map and type_map[df.fieldtype][0]=="varchar": | if df and df.fieldtype in type_map and type_map[df.fieldtype][0]=="varchar": | ||||
@@ -100,8 +100,9 @@ def sync_customizations_for_doctype(data): | |||||
update_schema = False | update_schema = False | ||||
def sync(key, custom_doctype, doctype_fieldname): | def sync(key, custom_doctype, doctype_fieldname): | ||||
frappe.db.sql('delete from `tab{0}` where `{1}`=%s'.format(custom_doctype, doctype_fieldname), | |||||
doctype) | |||||
doctypes = list(set(map(lambda row: row.get(doctype_fieldname), data[key]))) | |||||
frappe.db.sql('delete from `tab{0}` where `{1}` in ({2})'.format( | |||||
custom_doctype, doctype_fieldname, ",".join(["'%s'" % dt for dt in doctypes]))) | |||||
for d in data[key]: | for d in data[key]: | ||||
d['doctype'] = custom_doctype | d['doctype'] = custom_doctype | ||||
@@ -194,4 +194,5 @@ frappe.patches.v8_x.update_user_permission | |||||
frappe.patches.v8_5.patch_event_colors | frappe.patches.v8_5.patch_event_colors | ||||
frappe.patches.v8_10.delete_static_web_page_from_global_search | frappe.patches.v8_10.delete_static_web_page_from_global_search | ||||
frappe.patches.v8_x.add_bgn_xaf_xof_currencies | frappe.patches.v8_x.add_bgn_xaf_xof_currencies | ||||
frappe.patches.v9_1.add_sms_sender_name_as_parameters | |||||
frappe.patches.v9_1.add_sms_sender_name_as_parameters | |||||
execute:frappe.get_single('Domain Settings').save() |
@@ -23,6 +23,7 @@ class TestDomainification(unittest.TestCase): | |||||
frappe.db.sql("delete from `tabHas Role` where role='_Test Role'") | frappe.db.sql("delete from `tabHas Role` where role='_Test Role'") | ||||
frappe.db.sql("delete from tabDomain where name in ('_Test Domain 1', '_Test Domain 2')") | frappe.db.sql("delete from tabDomain where name in ('_Test Domain 1', '_Test Domain 2')") | ||||
frappe.delete_doc('DocType', 'Test Domainification') | frappe.delete_doc('DocType', 'Test Domainification') | ||||
self.remove_from_active_domains(remove_all=True) | |||||
def add_active_domain(self, domain): | def add_active_domain(self, domain): | ||||
""" add domain in active domain """ | """ add domain in active domain """ | ||||
@@ -14,6 +14,7 @@ from num2words import num2words | |||||
from six.moves import html_parser as HTMLParser | from six.moves import html_parser as HTMLParser | ||||
from six.moves.urllib.parse import quote, urljoin | from six.moves.urllib.parse import quote, urljoin | ||||
from html2text import html2text | from html2text import html2text | ||||
from markdown2 import markdown, MarkdownError | |||||
from six import iteritems, text_type, string_types, integer_types | from six import iteritems, text_type, string_types, integer_types | ||||
DATE_FORMAT = "%Y-%m-%d" | DATE_FORMAT = "%Y-%m-%d" | ||||
@@ -854,3 +855,19 @@ def to_markdown(html): | |||||
pass | pass | ||||
return text | return text | ||||
def to_html(markdown_text): | |||||
html = None | |||||
try: | |||||
html = markdown(markdown_text) | |||||
except MarkdownError: | |||||
pass | |||||
return html | |||||
def get_source_value(source, key): | |||||
'''Get value from source (object or dict) based on key''' | |||||
if isinstance(source, dict): | |||||
return source.get(key) | |||||
else: | |||||
return getattr(source, key) |