Compare commits

..

15 Commits

Author SHA1 Message Date
7dbc4ae55c Fix potential import error 2023-03-06 10:58:46 -08:00
401572996d Add catching for the error that can happen if an object type does not exist 2023-03-06 10:58:26 -08:00
3wc
f874ed1206 Switch to autonomic-cooperative civicrmapi4 2023-03-06 12:18:23 -05:00
d7cfc5419e Add Tag to the list of objects dumped and loaded. 2022-04-06 11:44:19 -07:00
62099efa85 Merge branch 'trunk' of ssh://git.autonomic.zone:2222/autonomic-cooperative/civicrm-confdump into trunk 2022-04-06 08:43:39 -07:00
851a784b36 Add custom field group association proper processing 2022-04-06 08:43:30 -07:00
107f10f704 Now importing saved searches so that smart group works 2022-03-23 18:36:51 +00:00
a341654334 and another 2022-03-23 17:55:17 +00:00
a91879cc08 fixed error in readme 2022-03-23 17:53:50 +00:00
e888c08d30 Merge branch 'trunk' of ssh://git.autonomic.zone:2222/autonomic-cooperative/civicrm-confdump into trunk 2022-03-10 08:11:03 -08:00
12d094c392 Fix array values in Contact for contact_sub_type 2022-03-10 08:10:53 -08:00
145a0f698d Changed operator for contact subtype to "contains"
When it was "=" it didn't get ones with multiple subtypes.
2022-03-09 09:39:29 +01:00
3344394fbc Merge branch 'trunk' of ssh://git.autonomic.zone:2222/autonomic-cooperative/civicrm-confdump into trunk 2022-03-03 09:10:20 -08:00
7c26e31da1 Add custom-field value table creation 2022-03-03 09:10:11 -08:00
5928295f37 added utf8 support & added confdump.venv to gitignore 2022-03-01 17:32:30 +01:00
4 changed files with 103 additions and 17 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
# security leaks?; # security leaks?;
.env .env
mydata mydata
confdump.venv
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/

View File

@ -5,9 +5,9 @@
- Install requirements `pip install -r requirements.txt` - Install requirements `pip install -r requirements.txt`
- `cp example.env .env` - `cp example.env .env`
- Edit `.env` and add creds for the production, test or development server you'll retrieve the data from. - Edit `.env` and add creds for the production, test or development server you'll retrieve the data from.
- Execute dump to retrieve base data: `env $(cat example.env) python ./confdump.py dump -o mydata INSTANCEPATH` where INSTANCEPATH is something like https://crm.dev.caat.org.uk/. - Execute dump to retrieve base data: `env $(cat .env) python ./confdump.py dump -o mydata INSTANCEPATH` where INSTANCEPATH is something like https://crm.dev.caat.org.uk/.
- **IMPORTANT!!** - If you sourced the data from live, you must delete the `.env` file or delete the creds from inside it. If you leave them in it will negate the whole purpose of having this conf dump utility, which is to provide a way of creating a local site without any sensitive data. - **IMPORTANT!!** - If you sourced the data from live, you must delete the `.env` file or delete the creds from inside it. If you leave them in it will negate the whole purpose of having this conf dump utility, which is to provide a way of creating a local site without any sensitive data.
- If your civicrm is in a docker container, load data into running local instance with: `confdump.py mysql -i mydata/ -p 63306` - If your civicrm is in a docker container, load data into running local instance with: `python confdump.py mysql -i mydata/ -p 63306`
- Otherwise use `confdump.py mysql -i mydata/ --host=<host> --db=<db> --user=<user> --password=<password>` - Otherwise use `confdump.py mysql -i mydata/ --host=<host> --db=<db> --user=<user> --password=<password>`
- Clear the cache in CiviCRM: - Clear the cache in CiviCRM:
* DOCKER: `make shell`, and then inside the shell `cd /app; ./vendor/bin/drush cc all` * DOCKER: `make shell`, and then inside the shell `cd /app; ./vendor/bin/drush cc all`

View File

@ -20,8 +20,10 @@ import traceback
import MySQLdb as mysql import MySQLdb as mysql
from typing import Any, Dict from collections import defaultdict
from typing import Any, Dict, List
import civicrmapi4
from civicrmapi4.civicrmapi4 import APIv4 from civicrmapi4.civicrmapi4 import APIv4
import phpserialize import phpserialize
@ -38,7 +40,9 @@ DUMP_TRIVIAL = ["FinancialType",
"CustomGroup", "CustomGroup",
"OptionGroup", "OptionGroup",
"OptionValue", "OptionValue",
"Domain"] "Domain",
"SavedSearch",
"Tag"]
# "ContributionPage", needs payment processors & payment_processor column formatted correctly. # "ContributionPage", needs payment processors & payment_processor column formatted correctly.
@ -63,7 +67,14 @@ LOAD_TRIVIAL = ["FinancialType",
"OptionGroup", "OptionGroup",
"OptionValue", "OptionValue",
"Domain", "Domain",
"Contact"] "Contact",
"SavedSearch",
"Tag"]
WEIRD_LIST = [
("civicrm_contact", "contact_sub_type"),
("civicrm_custom_group", "extends_entity_column_value")
]
# This is a payment processor we can assign contribution pages to in order for them to work. # This is a payment processor we can assign contribution pages to in order for them to work.
# FIXME this seems to produce a non-working setup. # FIXME this seems to produce a non-working setup.
@ -88,12 +99,37 @@ STANDIN_PAYMENT_PROCESSOR = {"id": "7",
"payment_instrument_id": "9"} "payment_instrument_id": "9"}
CUSTOM_FIELD_TYPE_MAP = {
"String": "VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL",
"Integer": "INT(11) DEFAULT NULL",
"Int": "INT(11) DEFAULT NULL",
"Memo": "VARCHAR(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL",
"Date": "datetime DEFAULT NULL",
"Boolean": "tinyint(4) DEFAULT NULL",
"Money": "decimal(20, 2) DEFAULT NULL",
"ContactReference": "INT(10) DEFAULT NULL"
}
def object_to_table(instr: str) -> str: def object_to_table(instr: str) -> str:
"""
"""
words = re.findall(r'[A-Z](?:[a-z]+|[A-Z]*(?=[A-Z]|$))', instr) words = re.findall(r'[A-Z](?:[a-z]+|[A-Z]*(?=[A-Z]|$))', instr)
return 'civicrm_' + '_'.join([x.lower() for x in words]) return 'civicrm_' + '_'.join([x.lower() for x in words])
def array_to_weird_array(val: List) -> str:
if (val is None):
return "NULL"
return '"\x01' + ('\x01'.join([str(x) for x in val])) + '\x01"'
def value_to_php_serialized(val: Any) -> str:
return "'{}'".format(mysql.escape_string(phpserialize.dumps(val).decode()).decode())
def python_value_to_sql(val: Any) -> str: def python_value_to_sql(val: Any) -> str:
"""
"""
if type(val) == bool: if type(val) == bool:
if val: if val:
return "TRUE" return "TRUE"
@ -103,18 +139,45 @@ def python_value_to_sql(val: Any) -> str:
if (isinstance(val, (int, float, complex))): if (isinstance(val, (int, float, complex))):
return str(val) return str(val)
if (type(val) == list): if (type(val) == list):
return "'{}'".format(",".join([str(v) for v in val])) # weird list serialization
return "'" + ','.join([str(x) for x in val]) + "'"
if (type(val) == dict): if (type(val) == dict):
return "'{}'".format(mysql.escape_string(phpserialize.dumps(val).decode()).decode()) return value_to_php_serialized(val)
return "'{}'".format(mysql.escape_string(val).decode()) return "'{}'".format(mysql.escape_string(val).decode())
def dict_to_insert(table: str, objdict: Dict) -> str: def dict_to_insert(table: str, objdict: Dict) -> str:
"""
"""
columns = tuple(x for x in objdict.keys()) columns = tuple(x for x in objdict.keys())
values = tuple(python_value_to_sql(objdict[x]) for x in columns) values = list()
for col in columns:
# any weird array we have to process here if there are others
if (table, col) in WEIRD_LIST:
values.append(array_to_weird_array(objdict[col]))
elif table == "civicrm_saved_search" and col == "form_values":
values.append(value_to_php_serialized(objdict[col]))
else:
values.append(python_value_to_sql(objdict[col]))
return "REPLACE INTO {} ({}) VALUES ({});".format(table, ",".join(columns), ",".join(values)) return "REPLACE INTO {} ({}) VALUES ({});".format(table, ",".join(columns), ",".join(values))
def create_custom_value_table(custom_group_record: Dict, custom_value_rows: List[Dict]) -> str:
"""
Return a sequence of SQL commands to create the custom value table based on a civicrm_custom_group record and a
series of value rows.
"""
fields = ",".join(["{} {}".format(x["column_name"], CUSTOM_FIELD_TYPE_MAP[x["data_type"]]) for x in custom_value_rows])
return """DROP TABLE IF EXISTS {cgr_name}; CREATE TABLE {cgr_name} (
id INT(10) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
entity_id int(10) unsigned NOT NULL,
{fields},
UNIQUE(entity_id)
);""".format(cgr_name=custom_group_record['table_name'], fields=fields)
def parse_arguments() -> argparse.Namespace: def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(prog="confdump.py", parser = argparse.ArgumentParser(prog="confdump.py",
@ -160,6 +223,16 @@ def parse_arguments() -> argparse.Namespace:
return parser.parse_args() return parser.parse_args()
def wrap_api_get(api, obj, args=None):
if args is None:
args = []
try:
return api.get(obj, args)
except civicrmapi4.civicrmapi4.CallFailed:
logging.error("Could not fetch {}".format(obj))
return []
def main() -> int: def main() -> int:
args = parse_arguments() args = parse_arguments()
@ -199,15 +272,14 @@ def main() -> int:
for table in DUMP_TRIVIAL: for table in DUMP_TRIVIAL:
output = args.output / (table + ".json") output = args.output / (table + ".json")
data = api.get(table) data = wrap_api_get(api, table)
if data: if data:
print("dumping", table) print("dumping", table)
with output.open("w") as of: with output.open("w") as of:
of.write(json.dumps(data)) of.write(json.dumps(data))
# dump org contacts
# dump org contacts
output = args.output / ("Contact.json") output = args.output / ("Contact.json")
data = api.get("Contact", where=[["contact_sub_type", "=", "Political_Party"]]) data = wrap_api_get(api, "Contact", where=[["contact_sub_type", "CONTAINS", "Political_Party"]])
if data: if data:
print("dumping parties") print("dumping parties")
with output.open("w") as of: with output.open("w") as of:
@ -218,16 +290,15 @@ def main() -> int:
print("input directory does not exist") print("input directory does not exist")
return 1 return 1
connection = mysql.connect(host=args.host, port=args.port, user=args.user, password=args.password, db=args.db) connection = mysql.connect(host=args.host, port=args.port, user=args.user, password=args.password, db=args.db, charset='utf8', use_unicode=True)
connection.autocommit(True) connection.autocommit(True)
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute("SET FOREIGN_KEY_CHECKS=0;") cursor.execute("SET FOREIGN_KEY_CHECKS=0;")
query = dict_to_insert("civicrm_payment_processor", STANDIN_PAYMENT_PROCESSOR) query = dict_to_insert("civicrm_payment_processor", STANDIN_PAYMENT_PROCESSOR)
print(query)
print(cursor.execute(query))
for table in LOAD_TRIVIAL: for table in LOAD_TRIVIAL:
# exceptions that require extra processing # exceptions that require extra processing
print(table)
with open((args.input / (table + ".json"))) as inf: with open((args.input / (table + ".json"))) as inf:
indata = json.load(inf) indata = json.load(inf)
table_name = object_to_table(table) table_name = object_to_table(table)
@ -235,9 +306,23 @@ def main() -> int:
if table == "ContributionPage": if table == "ContributionPage":
for row in indata: for row in indata:
row['payment_processor'] = STANDIN_PAYMENT_PROCESSOR_ID row['payment_processor'] = STANDIN_PAYMENT_PROCESSOR_ID
tot = 0;
for row in indata: for row in indata:
query = dict_to_insert(table_name, row) query = dict_to_insert(table_name, row)
cursor.execute(query) tot += cursor.execute(query)
print(tot)
# create custom field data tables
custom_fields = defaultdict(list)
with open((args.input / "CustomField.json")) as inf:
for field in json.load(inf):
custom_fields[field["custom_group_id"]].append(field)
with open((args.input / "CustomGroup.json")) as inf:
custom_groups = json.load(inf)
for group in custom_groups:
cursor.execute(create_custom_value_table(group, custom_fields[group["id"]]))
cursor.execute("SET FOREIGN_KEY_CHECKS=1;") cursor.execute("SET FOREIGN_KEY_CHECKS=1;")
cursor.close() cursor.close()

View File

@ -1,3 +1,3 @@
-e git+https://git.autonomic.zone/cas/civicrmapi4#egg=civicrmapi4 -e git+https://git.autonomic.zone/autonomic-cooperative/civicrmapi4#egg=civicrmapi4
phpserialize phpserialize
mysqlclient~=1.4.6 mysqlclient~=1.4.6