Commit 4fea4f31 authored by Nic's avatar Nic Committed by Aegir user
Browse files

events participants extension

parent 3302bfff
This diff is collapsed.
# View My Event Participants
## Overview
View My Event Participants (org.civicrm.vieweventparticipants) is a CiviCRM extension granting access for event creators to view or edit their events' participants.
## Why would you need this extension?
This extension is only needed if you have users who create events but who do not have permission to view or edit all the participant contacts of those events. I.e. the event creators do not have "view all contacts" or "edit all contacts" permission and do not have permission via ACLs (access control lists) to view or edit all the relevant contacts.
## What does this extension do?
It implements two new permissions: 'view my event participants' and 'edit my event participants'. These grant the user access to all the contacts who are participants of events that were created by the user.
**N.B.** If other ACLs are in place, e.g. through CiviCRM's "Manage ACLs" user interface, then this extension allows access to the user's events' participants _in addition to_ the contacts permitted by these other ACLs. Consider whether this is what you want: in some use cases, you might instead want to grant access to view/edit only the participants who are also permitted by the other ACLs.
E.g. suppose there's an existing ACL that gives a user access to all contacts with a UK address. With this extension, that user would have access to all UK contacts _plus_ all participants of events created by the user - even if those participants are outside the UK.
**Note:** access is granted by means of a hook. Consequently this will not show up in the CiviCRM user interface under Manage ACLs: it's done "under the hood".
## How do I use it?
1. Install the extension.
Currently this extension is not available for automated distribution through CiviCRM's Extension management screen and so must be manually downloaded onto the server, from https://github.com/circleinteractive/org.civicrm.vieweventparticipants and unpacked into your CiviCRM extensions directory. See https://docs.civicrm.org/sysadmin/en/latest/customize/extensions/ .
2. Enable the extension.
From the CiviCRM menu, go to Administer -> System settings -> Extensions. Click the **Enable** link for View Event Participants (org.civicrm.vieweventparticipants).
3. Grant permissions.
In your CMS's permissions management screen, grant the 'view my event participants' or 'edit my event participants' permissions to the users/roles who need to be able to view/edit their event participants.
<?xml version="1.0"?>
<extension key="org.civicrm.vieweventparticipants" type="module">
<file>vieweventparticipants</file>
<name>View My Event Participants</name>
<description>Grants access for event creators to view or edit their events' participants.</description>
<license>AGPL-3.0</license>
<maintainer>
<author>Dave Jenkins</author>
<email>davej@circle-interactive.co.uk</email>
</maintainer>
<urls>
<url desc="Main Extension Page">https://github.com/circleinteractive/org.civicrm.vieweventparticipants</url>
<url desc="Documentation">https://github.com/circleinteractive/org.civicrm.vieweventparticipants/blob/master/README.md</url>
<url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
</urls>
<releaseDate>2017-10-20</releaseDate>
<version>1.0.beta1</version>
<develStage>beta</develStage>
<compatibility>
<ver>4.7</ver>
</compatibility>
<comments>This is a new extension but has undergone extensive testing, including a suite of unit tests.</comments>
<civix>
<namespace>CRM/Vieweventparticipants</namespace>
</civix>
</extension>
<?xml version="1.0"?>
<phpunit backupGlobals="false" backupStaticAttributes="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" syntaxCheck="false" bootstrap="tests/phpunit/bootstrap.php">
<testsuites>
<testsuite name="My Test Suite">
<directory>./tests/phpunit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">./</directory>
</whitelist>
</filter>
<listeners>
<listener class="Civi\Test\CiviTestListener">
<arguments/>
</listener>
</listeners>
</phpunit>
# Tests for org.civicrm.vieweventparticipants
**To run the tests:**
1. Set up a development build of CiviCRM, e.g. using [buildkit](https://docs.civicrm.org/dev/en/latest/tools/buildkit/).
2. Install the org.civicrm.vieweventparticipants extension in the default extensions directory, files/civicrm/ext/ .
3. In a terminal, go into the directory `files/civicrm/ext/org.civicrm.vieweventparticipants` .
4. Specify the location of your CiviCRM settings file, e.g.
```export CIVICRM_SETTINGS=/Users/myuser/buildkit/build/dmaster/sites/default/civicrm.settings.php```
using the actual location of your civicrm.settings.php file.
5. Run the tests with:
```phpunit4 ./tests/phpunit/CRM/Vieweventparticipants/ACLTest.php```
<?php
use CRM_Vieweventparticipants_ExtensionUtil as E;
use Civi\Test\HeadlessInterface;
use Civi\Test\HookInterface;
use Civi\Test\TransactionalInterface;
/**
* Base class for unit tests for View My Event Participants extension (org.civicrm.vieweventparticipants).
*
* Tips:
* - With HookInterface, you may implement CiviCRM hooks directly in the test class.
* Simply create corresponding functions (e.g. "hook_civicrm_post(...)" or similar).
* - With TransactionalInterface, any data changes made by setUp() or test****() functions will
* rollback automatically -- as long as you don't manipulate schema or truncate tables.
* If this test needs to manipulate schema or truncate tables, then either:
* a. Do all that using setupHeadless() and Civi\Test.
* b. Disable TransactionalInterface, and handle all setup/teardown yourself.
*
* @group headless
*/
class CRM_Vieweventparticipants_BaseTestClass extends \PHPUnit_Framework_TestCase implements HeadlessInterface, HookInterface, TransactionalInterface {
private $_apiversion = 3;
/**
* @var CRM_Utils_Hook_UnitTests hookClass
* example of setting a method for a hook
* $this->hookClass->setHook('civicrm_aclWhereClause', array($this, 'aclWhereHookAllResults'));
*/
//public $hookClass = NULL;
public function setUpHeadless() {
// Civi\Test has many helpers, like install(), uninstall(), sql(), and sqlFile().
// See: https://github.com/civicrm/org.civicrm.testapalooza/blob/master/civi-test.md
return \Civi\Test::headless()
->installMe(__DIR__)
->apply();
}
public function setUp() {
//$this->hookClass = CRM_Utils_Hook::singleton(TRUE);
parent::setUp();
}
public function tearDown() {
parent::tearDown();
}
/***************************************************
* Utility functions copied from CiviUnitTestCase. *
* One day PHP traits will make this unnecessary. *
***************************************************/
/**
* Emulate a logged in user since certain functions use that.
* value to store a record in the DB (like activity)
* CRM-8180
*
* @return int
* Contact ID of the created user.
*/
public function createLoggedInUser() {
$params = array(
'first_name' => 'Logged In',
'last_name' => 'User ' . rand(),
'contact_type' => 'Individual',
);
$contactID = $this->individualCreate($params);
$this->callAPISuccess('UFMatch', 'create', array(
'contact_id' => $contactID,
'uf_name' => 'superman',
'uf_id' => 6,
));
$session = CRM_Core_Session::singleton();
$session->set('userID', $contactID);
return $contactID;
}
/**
* Check that api returned 'is_error' => 0.
*
* @param array $apiResult
* Api result.
* @param string $prefix
* Extra test to add to message.
*/
public function assertAPISuccess($apiResult, $prefix = '') {
if (!empty($prefix)) {
$prefix .= ': ';
}
$errorMessage = empty($apiResult['error_message']) ? '' : " " . $apiResult['error_message'];
if (!empty($apiResult['debug_information'])) {
$errorMessage .= "\n " . print_r($apiResult['debug_information'], TRUE);
}
if (!empty($apiResult['trace'])) {
$errorMessage .= "\n" . print_r($apiResult['trace'], TRUE);
}
$this->assertEquals(0, $apiResult['is_error'], $prefix . $errorMessage);
}
/**
* A stub for the API interface. This can be overriden by subclasses to change how the API is called.
*
* @param $entity
* @param $action
* @param array $params
* @return array|int
*/
public function civicrm_api($entity, $action, $params) {
return civicrm_api($entity, $action, $params);
}
/**
* wrap api functions.
* so we can ensure they succeed & throw exceptions without litterering the test with checks
*
* @param string $entity
* @param string $action
* @param array $params
* @param mixed $checkAgainst
* Optional value to check result against, implemented for getvalue,.
* getcount, getsingle. Note that for getvalue the type is checked rather than the value
* for getsingle the array is compared against an array passed in - the id is not compared (for
* better or worse )
*
* @return array|int
*/
public function callAPISuccess($entity, $action, $params, $checkAgainst = NULL) {
$params = array_merge(array(
'version' => $this->_apiversion,
'debug' => 1,
),
$params
);
switch (strtolower($action)) {
case 'getvalue':
return $this->callAPISuccessGetValue($entity, $params, $checkAgainst);
case 'getsingle':
return $this->callAPISuccessGetSingle($entity, $params, $checkAgainst);
case 'getcount':
return $this->callAPISuccessGetCount($entity, $params, $checkAgainst);
}
$result = $this->civicrm_api($entity, $action, $params);
$this->assertAPISuccess($result, "Failure in api call for $entity $action");
return $result;
}
/**
* This function exists to wrap api getValue function & check the result
* so we can ensure they succeed & throw exceptions without litterering the test with checks
* There is a type check in this
*
* @param string $entity
* @param array $params
* @param string $type
* Per http://php.net/manual/en/function.gettype.php possible types.
* - boolean
* - integer
* - double
* - string
* - array
* - object
*
* @return array|int
*/
public function callAPISuccessGetValue($entity, $params, $type = NULL) {
$params += array(
'version' => $this->_apiversion,
'debug' => 1,
);
$result = $this->civicrm_api($entity, 'getvalue', $params);
if ($type) {
if ($type == 'integer') {
// api seems to return integers as strings
$this->assertTrue(is_numeric($result), "expected a numeric value but got " . print_r($result, 1));
}
else {
$this->assertType($type, $result, "returned result should have been of type $type but was ");
}
}
return $result;
}
/**
* Generic function to create Individual, to be used in test cases
*
* @param array $params
* parameters for civicrm_contact_add api function call
* @param int $seq
* sequence number if creating multiple individuals
*
* @return int
* id of Individual created
*/
public function individualCreate($params = array(), $seq = 0) {
$params = array_merge($this->sampleContact('Individual', $seq), $params);
return $this->_contactCreate($params);
}
/**
* Helper function for getting sample contact properties.
*
* @param string $contact_type
* enum contact type: Individual, Organization
* @param int $seq
* sequence number for the values of this type
*
* @return array
* properties of sample contact (ie. $params for API call)
*/
public function sampleContact($contact_type, $seq = 0) {
$samples = array(
'Individual' => array(
// The number of values in each list need to be coprime numbers to not have duplicates
'first_name' => array('Anthony', 'Joe', 'Terrence', 'Lucie', 'Albert', 'Bill', 'Kim'),
'middle_name' => array('J.', 'M.', 'P', 'L.', 'K.', 'A.', 'B.', 'C.', 'D', 'E.', 'Z.'),
'last_name' => array('Anderson', 'Miller', 'Smith', 'Collins', 'Peterson'),
),
'Organization' => array(
'organization_name' => array(
'Unit Test Organization',
'Acme',
'Roberts and Sons',
'Cryo Space Labs',
'Sharper Pens',
),
),
'Household' => array(
'household_name' => array('Unit Test household'),
),
);
$params = array('contact_type' => $contact_type);
foreach ($samples[$contact_type] as $key => $values) {
$params[$key] = $values[$seq % count($values)];
}
if ($contact_type == 'Individual') {
$params['email'] = strtolower(
$params['first_name'] . '_' . $params['last_name'] . '@civicrm.org'
);
$params['prefix_id'] = 3;
$params['suffix_id'] = 3;
}
return $params;
}
/**
* Private helper function for calling civicrm_contact_add.
*
* @param array $params
* For civicrm_contact_add api function call.
*
* @throws Exception
*
* @return int
* id of Household created
*/
private function _contactCreate($params) {
$result = $this->callAPISuccess('contact', 'create', $params);
if (!empty($result['is_error']) || empty($result['id'])) {
throw new Exception('Could not create test contact, with message: ' . CRM_Utils_Array::value('error_message', $result) . "\nBacktrace:" . CRM_Utils_Array::value('trace', $result));
}
return $result['id'];
}
/**
* Create Participant.
*
* @param array $params
* Array of contact id and event id values.
*
* @return int
* $id of participant created
*/
public function participantCreate($params) {
if (empty($params['contact_id'])) {
$params['contact_id'] = $this->individualCreate();
}
if (empty($params['event_id'])) {
$event = $this->eventCreate();
$params['event_id'] = $event['id'];
}
$defaults = array(
'status_id' => 2,
'role_id' => 1,
'register_date' => 20070219,
'source' => 'Wimbeldon',
'event_level' => 'Payment',
'debug' => 1,
);
$params = array_merge($defaults, $params);
$result = $this->callAPISuccess('Participant', 'create', $params);
return $result['id'];
}
/**
* Create an Event.
*
* @param array $params
* Name-value pair for an event.
*
* @return array
*/
public function eventCreate($params = array()) {
// if no contact was passed, make up a dummy event creator
if (!isset($params['contact_id'])) {
$params['contact_id'] = $this->_contactCreate(array(
'contact_type' => 'Individual',
'first_name' => 'Event',
'last_name' => 'Creator',
));
}
// set defaults for missing params
$params = array_merge(array(
'title' => 'Annual CiviCRM meet',
'summary' => 'If you have any CiviCRM related issues or want to track where CiviCRM is heading, Sign up now',
'description' => 'This event is intended to give brief idea about progess of CiviCRM and giving solutions to common user issues',
'event_type_id' => 1,
'is_public' => 1,
'start_date' => 20081021,
'end_date' => 20081023,
'is_online_registration' => 1,
'registration_start_date' => 20080601,
'registration_end_date' => 20081015,
'max_participants' => 100,
'event_full_text' => 'Sorry! We are already full',
'is_monetary' => 0,
'is_active' => 1,
'is_show_location' => 0,
), $params);
return $this->callAPISuccess('Event', 'create', $params);
}
}
<?php
ini_set('memory_limit', '2G');
ini_set('safe_mode', 0);
eval(cv('php:boot --level=classloader', 'phpcode'));
// Allow autoloading of PHPUnit helper classes in this extension.
$loader = new \Composer\Autoload\ClassLoader();
$loader->add('CRM_', __DIR__);
$loader->add('Civi\\', __DIR__);
$loader->add('api_', __DIR__);
$loader->add('api\\', __DIR__);
$loader->register();
/**
* Call the "cv" command.
*
* @param string $cmd
* The rest of the command to send.
* @param string $decode
* Ex: 'json' or 'phpcode'.
* @return string
* Response output (if the command executed normally).
* @throws \RuntimeException
* If the command terminates abnormally.
*/
function cv($cmd, $decode = 'json') {
$cmd = 'cv ' . $cmd;
$descriptorSpec = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR);
$oldOutput = getenv('CV_OUTPUT');
putenv("CV_OUTPUT=json");
$process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__);
putenv("CV_OUTPUT=$oldOutput");
fclose($pipes[0]);
$result = stream_get_contents($pipes[1]);
fclose($pipes[1]);
if (proc_close($process) !== 0) {
throw new RuntimeException("Command failed ($cmd):\n$result");
}
switch ($decode) {
case 'raw':
return $result;
case 'phpcode':
// If the last output is /*PHPCODE*/, then we managed to complete execution.
if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") {
throw new \RuntimeException("Command failed ($cmd):\n$result");
}
return $result;
case 'json':
return json_decode($result, 1);
default:
throw new RuntimeException("Bad decoder format ($decode)");
}
}
<?php
// AUTO-GENERATED FILE -- Civix may overwrite any changes made to this file
/**
* (Delegated) Implements hook_civicrm_config().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_config
*/
function _vieweventparticipants_civix_civicrm_config(&$config = NULL) {
static $configured = FALSE;
if ($configured) {
return;
}
$configured = TRUE;
$template =& CRM_Core_Smarty::singleton();
$extRoot = dirname(__FILE__) . DIRECTORY_SEPARATOR;
$extDir = $extRoot . 'templates';
if ( is_array( $template->template_dir ) ) {
array_unshift( $template->template_dir, $extDir );
}
else {
$template->template_dir = array( $extDir, $template->template_dir );
}
$include_path = $extRoot . PATH_SEPARATOR . get_include_path( );
set_include_path($include_path);
}
/**
* (Delegated) Implements hook_civicrm_xmlMenu().
*
* @param $files array(string)
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_xmlMenu
*/
function _vieweventparticipants_civix_civicrm_xmlMenu(&$files) {
foreach (_vieweventparticipants_civix_glob(__DIR__ . '/xml/Menu/*.xml') as $file) {
$files[] = $file;
}
}
/**
* Implements hook_civicrm_install().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install
*/
function _vieweventparticipants_civix_civicrm_install() {
_vieweventparticipants_civix_civicrm_config();
if ($upgrader = _vieweventparticipants_civix_upgrader()) {
$upgrader->onInstall();
}
}
/**
* Implements hook_civicrm_uninstall().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall
*/
function _vieweventparticipants_civix_civicrm_uninstall() {
_vieweventparticipants_civix_civicrm_config();
if ($upgrader = _vieweventparticipants_civix_upgrader()) {
$upgrader->onUninstall();
}
}
/**
* (Delegated) Implements hook_civicrm_enable().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable
*/
function _viewe