mirror of
https://codeberg.org/artfulrobot/contactcats.git
synced 2025-06-25 23:18:05 +02:00
update tests, fix stuff, implement bespoke save, create actions for contactcategory
This commit is contained in:
parent
78e5b83c1d
commit
2ae781f6e3
6 changed files with 273 additions and 29 deletions
|
@ -8,12 +8,15 @@
|
||||||
*
|
*
|
||||||
* This stub provides compatibility. It is not intended to be modified in a
|
* This stub provides compatibility. It is not intended to be modified in a
|
||||||
* substantive way. Property annotations may be added, but are not required.
|
* substantive way. Property annotations may be added, but are not required.
|
||||||
|
*
|
||||||
|
* TODO: these are wrong, if it matters:
|
||||||
|
*
|
||||||
* @property string $id
|
* @property string $id
|
||||||
* @property string $contact_id
|
* @property string $contact_id
|
||||||
* @property string $category
|
* @property string $category
|
||||||
* @property string $next_category
|
* @property string $next_category
|
||||||
*/
|
*/
|
||||||
class CRM_Contactcats_DAO_ContactCategory extends CRM_Contactcats_DAO_Base {
|
class CRM_Contactcats_DAO_ContactCategory extends CRM_Core_DAO_Base {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required by older versions of CiviCRM (<5.74).
|
* Required by older versions of CiviCRM (<5.74).
|
||||||
|
|
28
Civi/Api4/Action/ContactCategory/Create.php
Normal file
28
Civi/Api4/Action/ContactCategory/Create.php
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
namespace Civi\Api4\Action\ContactCategory;
|
||||||
|
|
||||||
|
use Civi\Api4\Generic\Result;
|
||||||
|
use Civi\Api4\ContactCategory;
|
||||||
|
use CRM_Contactcats_ExtensionUtil as E;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create ContactCategory action
|
||||||
|
*
|
||||||
|
* This entity's ID is a unique primary key that references
|
||||||
|
* contact_id. Here we overwrite the validateValues which normally
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class Create extends \Civi\Api4\Generic\DAOCreateAction {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function _run(Result $result) {
|
||||||
|
// Wrap Save API
|
||||||
|
$saveResults = ContactCategory::save($this->getCheckPermissions())
|
||||||
|
->addRecord($this->values)
|
||||||
|
->execute()->getArrayCopy();
|
||||||
|
$result->exchangeArray($saveResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -72,6 +72,7 @@ class GetFlows extends \Civi\Api4\Generic\AbstractAction {
|
||||||
// $result['windowFunctionsSupported'] = $this->windowFunctionsSupported();
|
// $result['windowFunctionsSupported'] = $this->windowFunctionsSupported();
|
||||||
// if ($result['windowFunctionsSupported']) {
|
// if ($result['windowFunctionsSupported']) {
|
||||||
if ($this->windowFunctionsSupported()) {
|
if ($this->windowFunctionsSupported()) {
|
||||||
|
// $this->debug1($startDate, $endDate);
|
||||||
$this->solveWithWindowFunctions($result, $startDate, $endDate);
|
$this->solveWithWindowFunctions($result, $startDate, $endDate);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -114,19 +115,75 @@ class GetFlows extends \Civi\Api4\Generic\AbstractAction {
|
||||||
return $supported;
|
return $supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
protected function debug1(string $startDateYmd, ?string $endDateYmd) {
|
||||||
*
|
|
||||||
*/
|
|
||||||
protected function solveWithWindowFunctions(Result $result, string $startDateYmd, ?string $endDateYmd) {
|
|
||||||
|
|
||||||
$sql = <<<SQL
|
if (!$endDateYmd) {
|
||||||
|
$endDateYmd = date('Ymd', strtotime('tomorrow'));
|
||||||
|
}
|
||||||
|
$params = [1 => [$startDateYmd, 'Int'], 2 => [$endDateYmd, 'Int']];
|
||||||
|
$params1 = [1 => [$startDateYmd, 'Int']];
|
||||||
|
|
||||||
|
$this->dump("all activities", <<<SQL
|
||||||
|
SELECT ac.contact_id, a.activity_date_time, a.id activity_id
|
||||||
|
FROM civicrm_activity a
|
||||||
|
INNER JOIN civicrm_activity_contact ac
|
||||||
|
ON a.id = ac.activity_id AND ac.record_type_id = 3
|
||||||
|
WHERE a.activity_type_id = $this->activityTypeId
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
$this->dump("all start activities", <<<SQL
|
||||||
|
WITH activities AS (
|
||||||
|
SELECT ac.contact_id, a.activity_date_time, a.id activity_id
|
||||||
|
FROM civicrm_activity a
|
||||||
|
INNER JOIN civicrm_activity_contact ac
|
||||||
|
ON a.id = ac.activity_id AND ac.record_type_id = 3
|
||||||
|
WHERE a.activity_type_id = $this->activityTypeId
|
||||||
|
),
|
||||||
|
|
||||||
|
/* startActivity is the latest activity before the window's start date */
|
||||||
|
startActivity AS (
|
||||||
|
SELECT contact_id, activity_id,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY (contact_id)
|
||||||
|
ORDER BY activity_date_time DESC
|
||||||
|
) rn
|
||||||
|
FROM activities a1
|
||||||
|
WHERE a1.activity_date_time < %1
|
||||||
|
)
|
||||||
|
|
||||||
|
select * from startActivity;
|
||||||
|
SQL, $params1);
|
||||||
|
|
||||||
|
$this->dump("all end activities", <<<SQL
|
||||||
|
WITH activities AS (
|
||||||
|
SELECT ac.contact_id, a.activity_date_time, a.id activity_id
|
||||||
|
FROM civicrm_activity a
|
||||||
|
INNER JOIN civicrm_activity_contact ac
|
||||||
|
ON a.id = ac.activity_id AND ac.record_type_id = 3
|
||||||
|
WHERE a.activity_type_id = $this->activityTypeId
|
||||||
|
),
|
||||||
|
|
||||||
|
/* endActivity is the latest activity before the window's end date */
|
||||||
|
endActivity AS (
|
||||||
|
SELECT contact_id, activity_id,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY (contact_id)
|
||||||
|
ORDER BY activity_date_time DESC
|
||||||
|
) rn
|
||||||
|
FROM activities a2
|
||||||
|
WHERE a2.activity_date_time < %2
|
||||||
|
)
|
||||||
|
|
||||||
|
select * from endActivity;
|
||||||
|
SQL, $params);
|
||||||
|
|
||||||
|
$this->dump("all 2", <<<SQL
|
||||||
/* Identify the relevant activities for the contacts */
|
/* Identify the relevant activities for the contacts */
|
||||||
WITH activities AS (
|
WITH activities AS (
|
||||||
SELECT ac.contact_id, a.activity_date_time, a.id activity_id
|
SELECT ac.contact_id, a.activity_date_time, a.id activity_id
|
||||||
FROM civicrm_activity a
|
FROM civicrm_activity a
|
||||||
INNER JOIN civicrm_activity_contact ac
|
INNER JOIN civicrm_activity_contact ac
|
||||||
ON a.id = ac.activity_id AND ac.record_type_id = 3
|
ON a.id = ac.activity_id AND ac.record_type_id = 3
|
||||||
/*INNER JOIN civicrm_contact_category cc ON cc.id = ac.contact_id */
|
|
||||||
WHERE a.activity_type_id = $this->activityTypeId
|
WHERE a.activity_type_id = $this->activityTypeId
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -149,7 +206,68 @@ class GetFlows extends \Civi\Api4\Generic\AbstractAction {
|
||||||
ORDER BY activity_date_time DESC
|
ORDER BY activity_date_time DESC
|
||||||
) rn
|
) rn
|
||||||
FROM activities a2
|
FROM activities a2
|
||||||
WHERE a2.activity_date_time BETWEEN %1 AND %2
|
WHERE a2.activity_date_time < %2
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT /*startCat.new_category_id from_category_id, endCat.new_category_id to_category_id,*/
|
||||||
|
endActivity.contact_id endCtID, endActivity.activity_id endAcID,
|
||||||
|
startActivity.contact_id startCtID, startActivity.activity_id startAcID
|
||||||
|
FROM endActivity
|
||||||
|
/* INNER JOIN $this->catChangesTableName endCat
|
||||||
|
ON endActivity.activity_id = endCat.entity_id */
|
||||||
|
LEFT JOIN (
|
||||||
|
startActivity
|
||||||
|
/* INNER JOIN $this->catChangesTableName startCat
|
||||||
|
ON startActivity.activity_id = startCat.entity_id */
|
||||||
|
)
|
||||||
|
ON startActivity.contact_id = endActivity.contact_id AND startActivity.rn = 1
|
||||||
|
WHERE endActivity.rn = 1
|
||||||
|
/*ORDER BY from_category_id, to_category_id*/
|
||||||
|
;
|
||||||
|
SQL, $params);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This SQL uses the change activities to determine the flows between two dates.
|
||||||
|
*
|
||||||
|
* It does so by finding the latest activity before the start date and using it's new_category_id
|
||||||
|
* as the category for that contact on that date, and likewise for the end date.
|
||||||
|
*
|
||||||
|
* It is assumed that a change activity is always present.
|
||||||
|
*/
|
||||||
|
protected function solveWithWindowFunctions(Result $result, string $startDateYmd, ?string $endDateYmd) {
|
||||||
|
|
||||||
|
$sql = <<<SQL
|
||||||
|
/* Identify the relevant activities for the contacts */
|
||||||
|
WITH activities AS (
|
||||||
|
SELECT ac.contact_id, a.activity_date_time, a.id activity_id
|
||||||
|
FROM civicrm_activity a
|
||||||
|
INNER JOIN civicrm_activity_contact ac
|
||||||
|
ON a.id = ac.activity_id AND ac.record_type_id = 3
|
||||||
|
WHERE a.activity_type_id = $this->activityTypeId
|
||||||
|
),
|
||||||
|
|
||||||
|
/* startActivity is the latest activity before the window's start date */
|
||||||
|
startActivity AS (
|
||||||
|
SELECT contact_id, activity_id,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY (contact_id)
|
||||||
|
ORDER BY activity_date_time DESC
|
||||||
|
) rn
|
||||||
|
FROM activities a1
|
||||||
|
WHERE a1.activity_date_time < %1
|
||||||
|
),
|
||||||
|
|
||||||
|
/* endActivity is the latest activity before the window's end date */
|
||||||
|
endActivity AS (
|
||||||
|
SELECT contact_id, activity_id,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY (contact_id)
|
||||||
|
ORDER BY activity_date_time DESC
|
||||||
|
) rn
|
||||||
|
FROM activities a2
|
||||||
|
WHERE a2.activity_date_time < %2
|
||||||
)
|
)
|
||||||
|
|
||||||
/* join startActivity and endActivity to count changes between each shift */
|
/* join startActivity and endActivity to count changes between each shift */
|
||||||
|
@ -188,11 +306,21 @@ class GetFlows extends \Civi\Api4\Generic\AbstractAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function dump(string $msg, string $sql) {
|
/**
|
||||||
print "\n$msg ===============================\n$sql\n";
|
* useful in debugging phpunit tests only; unused in normal operation.
|
||||||
$data = CRM_Core_DAO::executeQuery($sql)->fetchAll();
|
*/
|
||||||
print_r($data);
|
protected function dump(string $msg, string $sql, $params = []) {
|
||||||
print "\n ===============================\n";
|
try {
|
||||||
|
$data = CRM_Core_DAO::executeQuery($sql, $params)->fetchAll();
|
||||||
|
}
|
||||||
|
catch (\Exception $e) {
|
||||||
|
print $e->getCause()->userinfo;
|
||||||
|
}
|
||||||
|
print "\n$msg (" . count($data) . ") records ===============================\n";
|
||||||
|
foreach ($data as $row) {
|
||||||
|
print json_encode($row) . "\n";
|
||||||
|
}
|
||||||
|
print "===============================\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
46
Civi/Api4/Action/ContactCategory/Save.php
Normal file
46
Civi/Api4/Action/ContactCategory/Save.php
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
namespace Civi\Api4\Action\ContactCategory;
|
||||||
|
|
||||||
|
use Civi\Api4\Generic\Result;
|
||||||
|
use CRM_Contactcats_ExtensionUtil as E;
|
||||||
|
use CRM_Core_DAO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create ContactCategory action
|
||||||
|
*
|
||||||
|
* This entity's ID is a unique primary key that references
|
||||||
|
* contact_id. Here we overwrite the validateValues which normally
|
||||||
|
*
|
||||||
|
* @see \Civi\Api4\Generic\AbstractAction
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class Save extends \Civi\Api4\Generic\DAOSaveAction {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow creating records with ids.
|
||||||
|
*
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function _run(Result $result) {
|
||||||
|
$ids = [];
|
||||||
|
foreach (array_column($this->records, 'id') as $id) {
|
||||||
|
if ($id) {
|
||||||
|
$ids[] = (int) $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($ids) {
|
||||||
|
$idsList = implode(",", array_filter($ids));
|
||||||
|
$preExisting = array_column(CRM_Core_DAO::executeQuery(
|
||||||
|
"SELECT id FROM civicrm_contact_category WHERE id IN ($idsList)"
|
||||||
|
)->fetchAll(), 'id');
|
||||||
|
// Any where we were given an ID that doesn't exist are important.
|
||||||
|
foreach (array_diff($ids, $preExisting) as $unknownID) {
|
||||||
|
CRM_Core_DAO::executeQuery("INSERT INTO civicrm_contact_category (id) VALUES ($unknownID)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now let it proceed as normal.
|
||||||
|
parent::_run($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ namespace Civi\Api4;
|
||||||
|
|
||||||
use Civi\Api4\Action\ContactCategory\GetFlows;
|
use Civi\Api4\Action\ContactCategory\GetFlows;
|
||||||
use Civi\Api4\Action\ContactCategory\Sync;
|
use Civi\Api4\Action\ContactCategory\Sync;
|
||||||
|
use Civi\Api4\Action\ContactCategory\Create;
|
||||||
|
use Civi\Api4\Action\ContactCategory\Save;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ContactCategory entity.
|
* ContactCategory entity.
|
||||||
|
@ -13,6 +15,24 @@ use Civi\Api4\Action\ContactCategory\Sync;
|
||||||
*/
|
*/
|
||||||
class ContactCategory extends Generic\DAOEntity {
|
class ContactCategory extends Generic\DAOEntity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bool $checkPermissions
|
||||||
|
* @return \Civi\Api4\Action\ContactCategory\Create
|
||||||
|
*/
|
||||||
|
public static function create($checkPermissions = TRUE) {
|
||||||
|
return (new Create(static::getEntityName(), __FUNCTION__))
|
||||||
|
->setCheckPermissions($checkPermissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bool $checkPermissions
|
||||||
|
* @return \Civi\Api4\Action\ContactCategory\Save
|
||||||
|
*/
|
||||||
|
public static function save($checkPermissions = TRUE) {
|
||||||
|
return (new Save(static::getEntityName(), __FUNCTION__))
|
||||||
|
->setCheckPermissions($checkPermissions);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -96,15 +96,24 @@ class GetFlowsTest extends \PHPUnit\Framework\TestCase implements HeadlessInterf
|
||||||
])
|
])
|
||||||
->execute()->column('id');
|
->execute()->column('id');
|
||||||
|
|
||||||
// Create a contact
|
[$ctID, $ctID2] = Contact::save(FALSE)
|
||||||
$ctID = Contact::create(FALSE)
|
->setDefaults(['contact_type' => 'Individual'])
|
||||||
->setValues([
|
->setRecords([
|
||||||
'contact_type' => 'Individual',
|
['display_name' => 'Wilma'],
|
||||||
'display_name' => 'Wilma',
|
['display_name' => 'Fred'],
|
||||||
])->execute()->first()['id'];
|
])->execute()->column('id');
|
||||||
// print "xxxxx created ct $ctID\n";
|
|
||||||
|
|
||||||
// Create two activities.
|
// Put them in meh category. This is not required for the test,
|
||||||
|
// but makes the data realistic.
|
||||||
|
$ccIDs = ContactCategory::save(FALSE)
|
||||||
|
->setRecords([
|
||||||
|
['id' => $ctID, 'category_definition_id' => $mehCatID],
|
||||||
|
['id' => $ctID2, 'category_definition_id' => $mehCatID],
|
||||||
|
])->execute()->column('id');
|
||||||
|
$this->assertEquals([$ctID, $ctID2], $ccIDs);
|
||||||
|
// print "ccs\n" . implode("\n", array_map('json_encode', CRM_Core_DAO::executeQuery("SELECT * from civicrm_contact_category cc")->fetchAll())) . "\n---\n";
|
||||||
|
|
||||||
|
// Create two activities for ct1 and just one for ct2
|
||||||
$activityIDs = Activity::save(FALSE)
|
$activityIDs = Activity::save(FALSE)
|
||||||
->setDefaults([
|
->setDefaults([
|
||||||
'activity_type_id:name' => 'changed_contact_category',
|
'activity_type_id:name' => 'changed_contact_category',
|
||||||
|
@ -115,23 +124,33 @@ class GetFlowsTest extends \PHPUnit\Framework\TestCase implements HeadlessInterf
|
||||||
[
|
[
|
||||||
'activity_date_time' => '2025-01-01',
|
'activity_date_time' => '2025-01-01',
|
||||||
'Category_changes.new_category_id' => $amazingCatID,
|
'Category_changes.new_category_id' => $amazingCatID,
|
||||||
|
'subject' => "Fake subject: $ctID change to amazing from nothing",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'activity_date_time' => '2025-01-01',
|
||||||
|
'Category_changes.new_category_id' => $mehCatID,
|
||||||
|
'target_contact_id' => $ctID2,
|
||||||
|
'subject' => "Fake subject: $ctID2 change to meh from nothing",
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'activity_date_time' => '2025-02-01',
|
'activity_date_time' => '2025-02-01',
|
||||||
'Category_changes.new_category_id' => $mehCatID,
|
'Category_changes.new_category_id' => $mehCatID,
|
||||||
|
'subject' => "Fake subject: $ctID change to meh from amazing",
|
||||||
],
|
],
|
||||||
])->execute()->column('id');
|
])->execute()->column('id');
|
||||||
|
|
||||||
// $acts = Activity::get(FALSE)->addWhere('id', 'IN', $activityIDs)->execute()->getArrayCopy(); print_r($acts);
|
// ================
|
||||||
|
// At 3 Jan, we had 1 amazing, 1 meh
|
||||||
// FIXME: this is returning backwards.
|
// Later, 1 amazing moved to meh.
|
||||||
|
// Flows should return non-changes, too.
|
||||||
$flows = ContactCategory::getFlows()
|
$flows = ContactCategory::getFlows()
|
||||||
->setStartDate('2025-01-03')
|
->setStartDate('2025-01-03')
|
||||||
->execute()->getArrayCopy();
|
->execute()->getArrayCopy();
|
||||||
// print_r($flows);
|
|
||||||
$this->assertEquals([
|
$this->assertEquals([
|
||||||
['from_category_id' => $amazingCatID, 'to_category_id' => $mehCatID, 'contact_count' => 1],
|
['from_category_id' => $amazingCatID, 'to_category_id' => $mehCatID, 'contact_count' => 1],
|
||||||
|
['from_category_id' => $mehCatID, 'to_category_id' => $mehCatID, 'contact_count' => 1],
|
||||||
], $flows);
|
], $flows);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue