2021-11-22 22:52:03 +00:00
|
|
|
# CiviCRM APIv4 Configuration dumper
|
|
|
|
# Prepared for Autonomic Cooperative
|
|
|
|
# For the CAAT project
|
|
|
|
|
|
|
|
# This tool can dump configuration to a directory, and reload configuration from a directory
|
|
|
|
# into a specified CiviCRM instance.
|
|
|
|
|
|
|
|
|
|
|
|
# FIXME handle username, password
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import pathlib
|
2022-01-27 05:11:26 +00:00
|
|
|
import re
|
2021-11-22 22:52:03 +00:00
|
|
|
import requests
|
|
|
|
import sys
|
|
|
|
import traceback
|
|
|
|
|
2022-01-27 05:11:26 +00:00
|
|
|
import MySQLdb as mysql
|
|
|
|
|
|
|
|
from typing import Any, Dict
|
|
|
|
|
2021-11-22 22:52:03 +00:00
|
|
|
from civicrmapi4.civicrmapi4 import APIv4
|
|
|
|
|
2022-01-27 05:11:26 +00:00
|
|
|
import phpserialize
|
|
|
|
|
2021-11-22 22:52:03 +00:00
|
|
|
# Entities which are simply a matter of dumping and upserting without
|
|
|
|
# additional processing.
|
|
|
|
DUMP_TRIVIAL = ["FinancialType",
|
2022-01-27 05:11:26 +00:00
|
|
|
"LocationType",
|
2021-11-22 22:52:03 +00:00
|
|
|
"ContributionPage",
|
2022-01-27 05:11:26 +00:00
|
|
|
"ContactType",
|
|
|
|
"RelationshipType",
|
2021-11-22 22:52:03 +00:00
|
|
|
"Group",
|
2022-01-27 05:11:26 +00:00
|
|
|
"CustomField",
|
2021-11-22 22:52:03 +00:00
|
|
|
"CustomGroup",
|
2022-01-27 05:11:26 +00:00
|
|
|
"OptionGroup",
|
|
|
|
"OptionValue",
|
|
|
|
"Domain"]
|
|
|
|
|
2022-03-01 16:17:20 +00:00
|
|
|
|
2022-01-27 05:11:26 +00:00
|
|
|
# "ContributionPage", needs payment processors & payment_processor column formatted correctly.
|
|
|
|
# the payment_processor column is a string with an integer id in it
|
|
|
|
# create a stand-in payment processor, and set the payment_processor column to its id
|
|
|
|
# | 8 | 1 | Paypal | NULL | 3 | 1 | 0 | 1 | naomirosenberguk+ikm_api1.gmail.com | FBM93XJTWAK5ZSAB | AWkT50gtrA0iXnh55b939tXXlAFYAOXzvynv8B4pJTYjDdt44TAJwrSD |
|
|
|
|
# https://www.sandbox.paypal.com/
|
|
|
|
# |
|
|
|
|
# https://api-3t.sandbox.paypal.com/
|
|
|
|
# | NULL |
|
|
|
|
# https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif
|
|
|
|
# | NULL | Payment_PayPalImpl
|
|
|
|
|
|
|
|
LOAD_TRIVIAL = ["FinancialType",
|
|
|
|
"LocationType",
|
|
|
|
"ContactType",
|
|
|
|
"ContributionPage",
|
|
|
|
"RelationshipType",
|
|
|
|
"Group",
|
2021-11-22 22:52:03 +00:00
|
|
|
"CustomField",
|
2022-01-27 05:11:26 +00:00
|
|
|
"CustomGroup",
|
|
|
|
"OptionGroup",
|
2021-11-22 22:52:03 +00:00
|
|
|
"OptionValue",
|
2022-03-01 16:24:30 +00:00
|
|
|
"Domain",
|
|
|
|
"Contact"]
|
2022-01-27 05:11:26 +00:00
|
|
|
|
|
|
|
# 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.
|
|
|
|
STANDIN_PAYMENT_PROCESSOR_ID = "7"
|
|
|
|
STANDIN_PAYMENT_PROCESSOR = {"id": "7",
|
|
|
|
"domain_id": "1",
|
|
|
|
"name": "Paypal",
|
|
|
|
"payment_processor_type_id": "3",
|
|
|
|
"is_active": "1",
|
|
|
|
"is_default": "0",
|
|
|
|
"is_test": "1",
|
|
|
|
"user_name": "naomirosenberguk+ikm_api1.gmail.com",
|
|
|
|
"password": "FBM93XJTWAK5ZSAB",
|
|
|
|
"signature": "AWkT50gtrA0iXnh55b939tXXlAFYAOXzvynv8B4pJTYjDdt44TAJwrSD",
|
|
|
|
"url_site": "https://www.sandbox.paypal.com/",
|
|
|
|
"url_api": "https://api-3t.sandbox.paypal.com/",
|
|
|
|
"url_button": "https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif",
|
|
|
|
"class_name": "Payment_PayPalImpl",
|
|
|
|
"billing_mode": "2",
|
|
|
|
"is_recur": "1",
|
|
|
|
"payment_type": "1",
|
|
|
|
"payment_instrument_id": "9"}
|
|
|
|
|
|
|
|
|
|
|
|
def object_to_table(instr: str) -> str:
|
|
|
|
words = re.findall(r'[A-Z](?:[a-z]+|[A-Z]*(?=[A-Z]|$))', instr)
|
|
|
|
return 'civicrm_' + '_'.join([x.lower() for x in words])
|
|
|
|
|
|
|
|
|
|
|
|
def python_value_to_sql(val: Any) -> str:
|
|
|
|
if type(val) == bool:
|
|
|
|
if val:
|
|
|
|
return "TRUE"
|
|
|
|
return "FALSE"
|
|
|
|
if val is None:
|
|
|
|
return "NULL"
|
|
|
|
if (isinstance(val, (int, float, complex))):
|
|
|
|
return str(val)
|
|
|
|
if (type(val) == list):
|
|
|
|
return "'{}'".format(",".join([str(v) for v in val]))
|
|
|
|
if (type(val) == dict):
|
|
|
|
return "'{}'".format(mysql.escape_string(phpserialize.dumps(val).decode()).decode())
|
|
|
|
return "'{}'".format(mysql.escape_string(val).decode())
|
|
|
|
|
|
|
|
|
|
|
|
def dict_to_insert(table: str, objdict: Dict) -> str:
|
|
|
|
columns = tuple(x for x in objdict.keys())
|
|
|
|
values = tuple(python_value_to_sql(objdict[x]) for x in columns)
|
|
|
|
return "REPLACE INTO {} ({}) VALUES ({});".format(table, ",".join(columns), ",".join(values))
|
|
|
|
|
2021-11-22 22:52:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
def parse_arguments() -> argparse.Namespace:
|
|
|
|
parser = argparse.ArgumentParser(prog="confdump.py",
|
|
|
|
description=("Dump configuration from a CiviCRM instance or load a previously"
|
|
|
|
"dumped configuration."))
|
|
|
|
parser.add_argument("-v", "--verbose", help="Show debug output.", action="store_true")
|
|
|
|
subparsers = parser.add_subparsers(dest='command')
|
|
|
|
dump_sub = subparsers.add_parser('dump', help="Dump configurations.")
|
2022-01-27 05:11:26 +00:00
|
|
|
dump_sub.add_argument("baseurl",
|
|
|
|
help="The base URL for the target instance.")
|
|
|
|
|
2021-11-22 22:52:03 +00:00
|
|
|
dump_sub.add_argument("-o",
|
|
|
|
"--output",
|
|
|
|
help="Output directory (will be created if it does not exist).",
|
|
|
|
type=pathlib.Path,
|
|
|
|
default=pathlib.Path("."))
|
2022-01-27 05:11:26 +00:00
|
|
|
|
|
|
|
loadmysql_sub = subparsers.add_parser("mysql", help="Load configuration via direct MySQL connection")
|
|
|
|
loadmysql_sub.add_argument("-i",
|
2021-11-22 22:52:03 +00:00
|
|
|
"--input",
|
|
|
|
help="Input directory.",
|
|
|
|
type=pathlib.Path)
|
2022-01-27 05:11:26 +00:00
|
|
|
loadmysql_sub.add_argument("-d",
|
|
|
|
"--db",
|
|
|
|
help="Database to connect to",
|
|
|
|
default="civicrm")
|
|
|
|
loadmysql_sub.add_argument("-u",
|
|
|
|
"--user",
|
|
|
|
help="Username",
|
|
|
|
default="civicrm")
|
|
|
|
loadmysql_sub.add_argument("--password",
|
|
|
|
help="Password",
|
|
|
|
default="civicrm")
|
|
|
|
loadmysql_sub.add_argument("-p",
|
|
|
|
"--port",
|
|
|
|
help="Port",
|
|
|
|
type=int,
|
|
|
|
default=3306)
|
|
|
|
loadmysql_sub.add_argument("--host",
|
|
|
|
help="Host",
|
|
|
|
default="127.0.0.1")
|
2021-11-22 22:52:03 +00:00
|
|
|
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
args = parse_arguments()
|
|
|
|
|
|
|
|
if args.command == "dump":
|
2022-01-27 05:11:26 +00:00
|
|
|
if args.verbose:
|
|
|
|
try: # for Python 3
|
|
|
|
from http.client import HTTPConnection
|
|
|
|
except ImportError:
|
|
|
|
from httplib import HTTPConnection
|
|
|
|
HTTPConnection.debuglevel = 1
|
|
|
|
|
|
|
|
logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from requests
|
|
|
|
logging.getLogger().setLevel(logging.DEBUG)
|
|
|
|
requests_log = logging.getLogger("urllib3")
|
|
|
|
requests_log.setLevel(logging.DEBUG)
|
|
|
|
requests_log.propagate = True
|
|
|
|
|
|
|
|
|
|
|
|
api = APIv4(args.baseurl)
|
|
|
|
username = os.environ.get('CIVICRM_USERNAME', None)
|
|
|
|
password = os.environ.get('CIVICRM_PASSWORD', None)
|
|
|
|
|
|
|
|
if (username is None) or (password is None):
|
|
|
|
print("Need to specify username and password CIVICRM_USERNAME and CIVICRM_PASSWORD enivronments.")
|
|
|
|
return 1
|
|
|
|
api.login(username, password)
|
|
|
|
if api.session is None:
|
|
|
|
print("Login failed.")
|
|
|
|
return 1
|
|
|
|
print("log in successful")
|
|
|
|
|
2021-11-22 22:52:03 +00:00
|
|
|
if (not args.output.exists()):
|
|
|
|
args.output.mkdir(parents=True)
|
|
|
|
if (not args.output.is_dir()):
|
|
|
|
print("Output directory exists and is not a directory")
|
|
|
|
return 1
|
|
|
|
|
|
|
|
for table in DUMP_TRIVIAL:
|
|
|
|
output = args.output / (table + ".json")
|
|
|
|
data = api.get(table)
|
|
|
|
if data:
|
|
|
|
print("dumping", table)
|
|
|
|
with output.open("w") as of:
|
|
|
|
of.write(json.dumps(data))
|
|
|
|
|
2022-03-01 16:17:20 +00:00
|
|
|
# dump org contacts
|
|
|
|
output = args.output / ("Contact.json")
|
|
|
|
data = api.get("Contact", where=[["contact_sub_type", "=", "Political_Party"]])
|
|
|
|
if data:
|
|
|
|
print("dumping parties")
|
|
|
|
with output.open("w") as of:
|
|
|
|
of.write(json.dumps(data))
|
|
|
|
|
2022-01-27 05:11:26 +00:00
|
|
|
if (args.command in ("load", "mysql")):
|
|
|
|
if (not args.input.exists()):
|
|
|
|
print("input directory does not exist")
|
|
|
|
return 1
|
|
|
|
|
|
|
|
connection = mysql.connect(host=args.host, port=args.port, user=args.user, password=args.password, db=args.db)
|
|
|
|
connection.autocommit(True)
|
|
|
|
cursor = connection.cursor()
|
|
|
|
cursor.execute("SET FOREIGN_KEY_CHECKS=0;")
|
|
|
|
|
|
|
|
query = dict_to_insert("civicrm_payment_processor", STANDIN_PAYMENT_PROCESSOR)
|
|
|
|
print(query)
|
|
|
|
print(cursor.execute(query))
|
|
|
|
for table in LOAD_TRIVIAL:
|
|
|
|
# exceptions that require extra processing
|
|
|
|
with open((args.input / (table + ".json"))) as inf:
|
|
|
|
indata = json.load(inf)
|
|
|
|
table_name = object_to_table(table)
|
|
|
|
# upsert records into db
|
|
|
|
if table == "ContributionPage":
|
|
|
|
for row in indata:
|
|
|
|
row['payment_processor'] = STANDIN_PAYMENT_PROCESSOR_ID
|
|
|
|
for row in indata:
|
|
|
|
query = dict_to_insert(table_name, row)
|
2022-02-03 05:13:07 +00:00
|
|
|
cursor.execute(query)
|
2022-01-27 05:11:26 +00:00
|
|
|
cursor.execute("SET FOREIGN_KEY_CHECKS=1;")
|
|
|
|
cursor.close()
|
|
|
|
|
2021-11-22 22:52:03 +00:00
|
|
|
# main entry
|
|
|
|
if __name__ == "__main__":
|
|
|
|
sys.exit(main())
|