Skip to content

Commit

Permalink
Merge branch '1.7-wip' into la-milestone-3
Browse files Browse the repository at this point in the history
  • Loading branch information
igorpavlov committed Aug 21, 2017
2 parents 7365d2b + a63c927 commit 2fe28f7
Show file tree
Hide file tree
Showing 70 changed files with 4,511 additions and 3,616 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

use CRM_Hrjobroles_API_Query_VirtualEntitySelectQuery as VirtualEntitySelectQuery;
use CRM_Hrjobroles_BAO_HrJobRoles as HRJobRoles;
use CRM_Hrjobcontract_BAO_HRJobContract as HRJobContract;

/**
* This is a special class created mainly to support the ContactHrJobRoles.get
* API. Its main objective is to return a list of Job Roles, but with a reduced
* set of properties, and also include the id of the contact the role belongs to,
* which is an information that is not stored together with the Job Role. It does
* this by creating a query that joins the Job Roles table with the Contract
* table, where the contact ID exists.
*
* Internally it uses a child class of the Api3SelectQuery class, which means
* it can support everything that a normal .get query can like:
* - Different operators (IN, LIKE, <>, etc)
* - Limit and Offset operations
* - Sorting
*/
class CRM_Hrjobroles_API_Query_ContactHrJobRolesSelect {

/**
* @var array
* An array of params passed to an API endpoint
*/
private $params;

/**
* @var \CRM_HRLeaveAndAbsences_API_Query_Select
* The SelectQuery instance wrapped by this class
*/
private $query;

/**
* @var array
* A list of fields that are supported by the entity + their names as they
* should be added to the SELECT query. Usually the internal classes can
* build this by themselves, but here we need to do it manually because not
* all the fields are from the same database table
*/
private $selectableFields = [
'id' => 'a.id',
'title' => 'a.title',
'description' => 'a.description',
'region' => 'a.region',
'department' => 'a.department',
'level_type' => 'a.level_type',
'functional_area' => 'a.functional_area',
'location' => 'a.location',
'contact_id' => 'jc.contact_id'
];

public function __construct($params) {
$this->params = $params;
$this->buildCustomQuery();
}

/**
* Build the custom query.
*
* Here is basically where we get the given $params and build the entire query,
* including the JOIN with the Contracts table
*/
private function buildCustomQuery() {
$customQuery = CRM_Utils_SQL_Select::from(HRJobRoles::getTableName() . ' as a');

$this->addJoins($customQuery);

$this->query = $this->buildSelectQuery('ContactHrJobRoles');
$this->query->merge($customQuery);
}

/**
* This method parses the $params array passed to the API and build an instance
* of VirtualEntitySelectQuery, which is a child class of Api3SelectQuery and
* it's the class that will actually build the SQL query.
*
* @param string $entity
*
* @return \CRM_Hrjobroles_API_Query_VirtualEntitySelectQuery
*/
private function buildSelectQuery($entity) {
$checkPermissions = !empty($this->params['check_permissions']);
$query = new VirtualEntitySelectQuery($entity, $this->selectableFields, $checkPermissions);

$query->where = $this->params;

$options = _civicrm_api3_get_options_from_params($this->params);

if ($options['is_count']) {
$query->select = ['count_rows'];
}
else {
$query->select = array_keys(array_filter($options['return']));
$query->orderBy = $options['sort'];
}

$query->limit = $options['limit'];
$query->offset = $options['offset'];

return $query;
}

/**
* Add the clauses to JOIN the Job Roles table with other tables
*
* @param \CRM_Utils_SQL_Select $query
*/
private function addJoins(CRM_Utils_SQL_Select $query) {
$joins[] = 'INNER JOIN ' . HRJobContract::getTableName() . ' jc ON jc.id = a.job_contract_id';

$query->join(null, $joins);
}

/**
* Executes the query
*
* @return array|int
*/
public function run() {
return $this->query->run();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

use Civi\API\Api3SelectQuery;

/**
* This is a specialization of the Api3SelectQuery, created to allow an entity
* to return fields that are not part of an entity's table.
*
* The original implementation of the buildSelectFields() on the Api3SelectQuery
* class makes sure that only fields of the entity's table will be returned by
* the query. That means that, even if this is a query with multiple joins, it
* won't be possible to return fields from any of the joined tables. This class
* works around this by overriding that method and making it filter out fields
* based on a list of "allowed" fields passed to the constructor.
*/
class CRM_Hrjobroles_API_Query_VirtualEntitySelectQuery extends Api3SelectQuery {

/**
* @var array|bool
* The list of fields that can be selected/returned by the query
* Format: 'field name' => 'field name on the select clause (e.g. contract.contact_id)'
*/
protected $virtualEntityFields = [];

/**
* CRM_Hrjobroles_API_Query_VirtualEntitySelectQuery constructor.
*
* @param string $entity
* @param array $virtualEntityFields
* The list of fields that can be selected/returned
* @param bool $checkPermissions
*/
public function __construct($entity, $virtualEntityFields, $checkPermissions) {
parent::__construct($entity, $checkPermissions);
$this->virtualEntityFields = $virtualEntityFields;
}

/**
* Builds the list of fields that can be selected by the query, based on the
* list of fields passed to the constructor
*/
protected function buildSelectFields() {
$returnAllFields = (empty($this->select) || !is_array($this->select));
$return = $returnAllFields ? $this->entityFieldNames : $this->select;
if ($returnAllFields) {
foreach (array_keys($this->apiFieldSpec) as $fieldName) {
$return[] = $fieldName;
}
}

// Always select the ID to keep some consistency with the API3 behavior
$this->selectFields[self::MAIN_TABLE_ALIAS . '.id'] = 'id';

foreach ($return as $fieldName) {
$field = $this->getField($fieldName);
if ($field && in_array($field['name'], $this->entityFieldNames)) {
$this->selectFields[$this->virtualEntityFields[$fieldName]] = $field['name'];
}
}
}
}
68 changes: 68 additions & 0 deletions com.civicrm.hrjobroles/CRM/Hrjobroles/BAO/ContactHrJobRoles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

use CRM_Hrjobroles_DAO_HrJobRoles as HrJobRoles;

/**
* This is a virtual entity. Its main objective is to serve as the underlying
* entity for the ContactJobRole API.
*
* Originally, there's no direct connection between a Job Role and a Contact.
* A Job Role is linked to a Contract, which finally is the entity connect to
* a Contact. This means that, if we want to know which contacts work in the
* IT department, we need to:
* 1. Fetch all the Job Roles where the department == IT
* 2. Fetch all the Contracts linked to these Job Roles
* 3. Fetch all the Contacts linked to these Contracts
*
* The idea of this virtual entity is to expose both the Contact and Job Role
* information under a single place and reduce the number of API calls to get
* the data as exemplified above. The ContactJobRole.get custom API was created
* to create the query necessary for this. This virtual BAO is necessary to
* exposed which field will be available on the API.
*
* @see civicrm_api3_contact_job_role_get()
* @see CRM_Hrjobroles_API_Query_ContactHrJobRolesSelect
*/
class CRM_Hrjobroles_BAO_ContactHrJobRoles extends HrJobRoles {

/**
* @var array
* To avoid exposing confidential information via de API, this entity exposes
* just small set of all the HrJobRoles fields. Should a user have access to
* all the fields, the HrJobRoles.get API should be used instead.
* This is the list of fields exposed by the ContactJobRole.API
*/
private static $allowedFields = [
'id',
'title',
'region',
'department',
'level_type',
'location'
];

/**
* Returns the same information as HrJobRoles::fields(), but only for the
* fields listed on $allowedFields. Additionally, it returns a "fake"
* contact_id, which will actually come from the Job Contract.
*
* @return array
*/
public static function &fields() {
$fields = [];
foreach(HrJobRoles::fields() as $key => $field) {
if(!empty($field['name']) && in_array($field['name'], self::$allowedFields)) {
$fields[$key] = $field;
}
}

$fields['contact_id'] = [
'name' => 'contact_id',
'type' => CRM_Utils_Type::T_INT,
'title' => ts('Contact ID'),
];

return $fields;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

class CRM_Hrjobroles_Test_Fabricator_HrJobRoles {

protected static $defaultParams = [
'sequential' => 1
];

/**
* @param array $params
* An array of params that will be passed to the civicrm API
*
* @return array
* The entity values as they are returned by the API call
*
* @throws \Exception
*/
public static function fabricate($params) {
$result = civicrm_api3(
'HrJobRoles',
'create',
array_merge(self::$defaultParams, $params)
);

return array_shift($result['values']);
}
}
28 changes: 10 additions & 18 deletions com.civicrm.hrjobroles/CRM/Hrjobroles/Upgrader.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,32 +49,24 @@ public function upgrade_1004() {
* Creates new Option Group for Cost Centres
*/
public function installCostCentreTypes() {

function save($val, $key, $id){
civicrm_api3('OptionValue', 'create', array(
'sequential' => 1,
'option_group_id' => $id,
'label' => $val,
'value' => $val,
'name' => $val,
));
}

try{
$result = civicrm_api3('OptionGroup', 'create', array(
$result = civicrm_api3('OptionGroup', 'create', [
'sequential' => 1,
'name' => "cost_centres",
'title' => "Cost Centres",
'is_active' => 1
));
]);

$id = $result['id'];

$options = array(
'Other' => 'Other'
);

array_walk($options, 'save', $id);
$val = 'Other';
civicrm_api3('OptionValue', 'create', [
'sequential' => 1,
'option_group_id' => $id,
'label' => $val,
'value' => $val,
'name' => $val,
]);

} catch(Exception $e){
// OptionGroup already exists
Expand Down
29 changes: 29 additions & 0 deletions com.civicrm.hrjobroles/api/v3/ContactHrJobRoles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/**
* ContactJobRole.get API
*
* @param array $params
*
* @return array API result descriptor
*
* @throws API_Exception
*/
function civicrm_api3_contact_hr_job_roles_get($params) {
$query = new CRM_Hrjobroles_API_Query_ContactHrJobRolesSelect($params);

return civicrm_api3_create_success($query->run(), $params, 'ContactHrJobRoles', 'get');
}

/**
* This function is used internally, to respond to a call to
* ContactHrJobRoles.getFields. The CiviCRM will try to call a function with
* this name to get the DAO for this entity.
*
* This is necessary because there is no DAO for ContactHrJobRoles.
*
* @return string
*/
function _civicrm_api3_contact_hr_job_roles_DAO() {
return CRM_Hrjobroles_BAO_ContactHrJobRoles::class;
}
Loading

0 comments on commit 2fe28f7

Please sign in to comment.