drupal_module__migrateunfuddle/UnfuddleMigration.inc
Frederic G. MARAND f038250106 Issue #0 by fgm: added UnfuddleTicketFollowup migration
- can now import follow-ups on tickets
- still very crude, no attachments
2011-09-03 16:35:09 +02:00

606 lines
22 KiB
PHP

<?php
/**
* @file
* Migration class to import user accounts from an Unfuddle dump.
*
* @copyright Coyright (c) 2011 Ouest Systèmes Informatiques (OSI, OSInet)
*
* @license Licensed under the General Public License version 2 and later, and the CeCILL 2.0 license.
*/
/**
* Information/utility class about CaseTracker in a Migrate context.
*/
class CaseTrackerMeta {
public static function getFields() {
$fields = array(
'pid' => t('Case project'),
'case_number' => t('Case number'),
'assign_to' => t('Case assigned to'),
'case_priority_id' => t('Case priority'),
'case_type_id' => t('Case type'),
'case_status_id' => t('Case status'),
);
return $fields;
}
}
class MigrateDestinationCommentFollowup extends MigrateDestinationComment {
public function __construct() {
parent::__construct(MigrateDestinationNodeCase::ct_bundle);
}
/**
* Could also be implemented as a migrate hook_migrate_fields() in module.
* Which is best ?
*
* (non-PHPdoc)
* @see MigrateDestinationComment::fields()
*/
public function fields() {
$fields = parent::fields();
$fields['revision_id'] = t('Ticket revision ID');
$fields += CaseTrackerMeta::getFields();
return $fields;
}
public function prepare($entity, stdClass $source_row) {
$ticket = node_load($entity->nid);
// ticket->casetracker is assigned by CT to both new and old, so clone is
// required, to avoid overwriting one state with the other
$entity->casetracker = clone $ticket->casetracker;
// CT needs a name in assign_to, not an uid
$entity->casetracker->assign_to = casetracker_get_name($entity->casetracker->assign_to);
//print_r($entity);
$entity->revision_id = $ticket->vid;
}
}
/**
* Casetracker-specific destination node handling
*/
class MigrateDestinationNodeCase extends MigrateDestinationNode {
const ct_bundle = 'casetracker_basic_case';
public function __construct() {
parent::__construct(self::ct_bundle);
}
/**
* Could also be implemented as a migrate hook_migrate_fields() in module.
* Which is best ?
*
* (non-PHPdoc)
* @see MigrateDestinationNode::fields()
*/
public function fields() {
$fields = parent::fields();
$fields += CaseTrackerMeta::getFields();
return $fields;
}
/**
* Casetracker saves its fields in a separate stdClass extra field.
*
* (non-PHPdoc)
* @see MigrateDestinationEntity::prepare()
*/
public function prepare($entity, stdClass $source_row) {
$extraFields = array(
'pid',
'case_priority_id',
'case_type_id',
'assign_to',
'case_status_id',
);
$entity->casetracker = new stdClass();
foreach ($extraFields as $extra) {
if (isset($entity->$extra)) {
$entity->casetracker->$extra = $entity->$extra;
unset($entity->extra);
}
}
$entity->casetracker->case_type_id = 9; // Bug. Case type is not a default Unfuddle field
}
}
abstract class UnfuddleMigration extends XMLMigration {
protected static $fields = array();
public $item_ID_xpath = 'id';
public $items_url;
public $unmigratedDestinations = array();
public $unmigratedSources = array();
public function __construct($group) {
parent::__construct($group);
$this->items_url = file_directory_path() . '/unfuddle/backup.xml';
}
public function addXPathFieldMapping($destination_field, $source_field, $xpath = NULL) {
if (!isset($xpath)) {
$xpath = $source_field;
}
return $this->addFieldMapping($destination_field, $source_field)
->xpath($xpath);
}
public function afterConstruct() {
$this->addUnmigratedDestinations($this->unmigratedDestinations, t('White hole'));
$this->addUnmigratedSources($this->unmigratedSources, t('Black hole'));
}
/**
* Return a field-name-indexed hash of field descriptions.
*
* @return array
*/
protected static function getFieldInfo() {
return self::$fields;
}
}
/**
* Import "people" data from the Unfuddle backup into Drupal users
*/
class UnfuddlePeopleMigration extends UnfuddleMigration {
public $unmigratedDestinations = array(
'pass',
'roles',
'theme',
'signature',
'signature_format',
'language',
'picture',
);
public $unmigratedSources = array(
'account-id',
'first-name',
'identity-url',
'is-administrator',
'last-name',
'notification-frequency',
'notification-ignore-self',
'notification-last-sent',
'notification-scope-messages',
'notification-scope-milestones',
'notification-scope-notebooks',
'notification-scope-source',
'notification-scope-tickets',
'text-markup',
'updated-at',
);
public function __construct() {
parent::__construct(MigrateGroup::getInstance('Unfuddle'));
$item_xpath = '/account/people/person';
$items_class = new MigrateItemsXML($this->items_url, $item_xpath, $this->item_ID_xpath);
$this->source = new MigrateSourceMultiItems($items_class, self::getFieldInfo());
$source_key = array(
'id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
)
);
$this->destination = new MigrateDestinationUser();
$this->map = new MigrateSQLMap('unfuddle_people',
$source_key,
MigrateDestinationUser::getKeySchema());
// Do not map uid: this prevents user_save from creating the accounts.
$this->addFieldMapping('uid', NULL)
->defaultValue(0);
$this->addXPathFieldMapping('created', 'created-at');
$this->addXPathFieldMapping('mail', 'email');
$this->addXPathFieldMapping('init', 'email')
->defaultValue('support@osinet.fr')
->description(t('Unfuddle does not keep the original email address'));
$this->addXPathFieldMapping('status', 'is-removed')
->description(t('Inverted when converting to user.status'));
$this->addXPathFieldMapping('login', 'last-signed-in');
$this->addXPathFieldMapping('access', 'last-signed-in')
->description(t('Unfuddle does not log access time, only sign in.'));
$this->addXPathFieldMapping('timezone', 'time-zone')
->description(t('Only Paris and London currently supported, without DST'));
$this->addXPathFieldMapping('name', 'username');
$this->afterConstruct();
}
protected static function getFieldInfo() {
self::$fields = array(
'account-id' => t('Unfuddle account Id'),
'id' => t('User account ID'),
'created-at' => t('User account creation timestamp'),
'email' => t('Current email address for the account'),
'first-name' => t('User given name'),
'identity-url' => t('OpenID URL for the user account'),
'is-administrator' => t('User is an Unfuddle project administrator'),
'is-removed' => t('User account has been removed'),
'last-name' => t('User last name'),
'last-signed-in' => t('Latest login timestamp'),
'notification-frequency' => t('Unfuddle notification frequency'),
'notification-ignore-self' => t('Ignore self when sending notifications'),
'notification-last-sent' => t('Timestamp of latest notification sent'),
'notification-scope-messages' => t('Send message notifications'),
'notification-scope-milestones' => t('Send milestones notifications'),
'notification-scope-notebooks' => t('Send notebooks notifications'),
'notification-scope-source' => t('Send source notifications'),
'notification-scope-tickets' => t('Send tickets notifications'),
'text-markup' => t('Preferred markup format for issues'),
'time-zone' => t('Timezone'),
'updated-at' => t('Timestamp of latest user account change'),
'username' => t('The user account login name'),
);
return parent::getFieldInfo();
}
/**
* - Timestamp format conversion not needed, as per MigrationBase::timestamp()
* - Invert is-removed to convert to status
* - Convert Unfuddle timezones to Drupal offset seconds
*
* Do not forget type casts when reading SimpleXML elements.
*
* @param object $row
*/
public function prepareRow(stdClass $row) {
// Automatic via MigrationBase::timestamp()
// $row->xml->{'created-at'} = strtotime($row->xml->{'created-at'});
// $row->xml->{'last-signed-in'} = strtotime($row->xml->{'last-signed-in'});
$row->xml->{'is-removed'} = ((int) $row->xml->{'is-removed'}) ? 0 : 1;
// @todo TODO Primitive and does not account for DST
$timezones = array(
'London' => 0,
'Paris' => 7200,
);
$timezone = (string) $row->xml->{'time-zone'};
$row->xml->{'time-zone'} = empty($timezones[$timezone])
? 0
: $timezones[$timezone];
}
}
/**
* Import "project" core data from Unfuddle into CaseTracker project nodes
*/
class UnfuddleProjectMigration extends UnfuddleMigration {
public $unmigratedDestinations = array(
'promote',
'sticky',
'revision',
'language',
);
public $unmigratedSources = array(
'account-id',
'assignee-on-resolve',
'backup-frequency',
'close-ticket-simultaneously-default',
'default-ticket-report-id',
'disk-usage',
'enable-time-tracking',
's3-access-key-id',
's3-backup-enabled',
's3-bucket-name',
'theme',
'categories', // @todo TODO Taxonomy ?
);
public function __construct() {
parent::__construct(MigrateGroup::getInstance('Unfuddle'));
$item_xpath = '/account/projects/project';
$items_class = new MigrateItemsXML($this->items_url, $item_xpath, $this->item_ID_xpath);
$this->source = new MigrateSourceMultiItems($items_class, self::getFieldInfo());
$source_key = array(
'id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
)
);
$this->dependencies = array('UnfuddlePeople');
$this->destination = new MigrateDestinationNode('casetracker_basic_project');
$this->map = new MigrateSQLMap('unfuddle_project',
$source_key,
MigrateDestinationNode::getKeySchema());
$this->addXPathFieldMapping('created', 'created-at');
$this->addXPathFieldMapping('status', 'archived')
->description(t('Inverted when converting to node.status'));
$this->addXPathFieldMapping('body', 'description')
->description(t('Applying site default input format: will often be wrong'));
$this->addXPathFieldMapping('teaser', node_teaser('description', FILTER_FORMAT_DEFAULT))
->description(t('Applying site default input format: will often be wrong'));
$this->addXPathFieldMapping('changed', 'updated-at');
$this->addXPathFieldMapping('title', 'title');
$this->addXPathFieldMapping('uid', NULL)
->sourceMigration('UnfuddlePeople')
->defaultValue(1);
$this->addXPathFieldMapping('revision_uid', NULL)
->sourceMigration('UnfuddlePeople')
->defaultValue(1);
$this->addXPathFieldMapping('path', 'short-name')
->description(t('Auto-aliased to project/(short-name)'));
$this->afterConstruct();
}
protected static function getFieldInfo() {
self::$fields = array(
'account-id' => t('Unfuddle account Id'),
'archived' => t('Unpublished'),
'assignee-on-resolve' => t('The user being assigned a ticket once it is resolved'),
'backup-frequency' => t('Current email address for the account'),
'close-ticket-simultaneously-default' => t('Close tickets on Resolved status'),
'created-at' => t('Project creation timestamp'),
'default-ticket-report-id' => t('User reporting tickets by default'),
'description' => t('Description'),
'disk-usage' => t('Disk usage on Unfuddle, in bytes'),
'enable-time-tracking' => t('Enable issue time tracking'),
's3-access-key-id' => t('Amazon S3 key'),
's3-backup-enabled' => t('Periodic backups sent to Amazon S3'),
's3-bucket-name' => t('Amazon S3 destination bucket for backups'),
'short-name' => t('Short project name (machine name)'),
'theme' => t('Unfuddle main color for project theme'),
/**
* @todo TODO insert as CCK fields
*
'ticket-fieldn-active' => (boolean),
'ticket-fieldn-disposition' => 'list',
'ticket-fieldn-title' => (label)
*
*/
'title' => t('Title'),
'updated-at' => t('Timestamp of latest project change'),
'categories' => t('Categories'),
/*
* Ignored:
components
custom-field-values
messages
milestones
notebooks
severities
tickets -> separate import
ticket-reports (views)
versions
*
*/
);
return self::$fields;
}
public function preparerow(stdClass $row) {
$row->xml->{'archived'} = ((int) $row->xml->{'archived'}) ? 0 : 1;
$row->xml->{'short-name'} = 'project/'. drupal_clean_css_identifier($row->xml->{'short-name'});
}
}
/**
* Import ticket follow-up (comments) data from Unfuddle into CaseTracker project comments
*/
class UnfuddleTicketFollowupMigration extends UnfuddleMigration {
public $unmigratedDestinations = array(
'hostname',
'status',
'thread',
'name',
'mail',
'homepage',
'language',
'revision_id',
);
public $unmigratedSources = array(
'created-at',
'body-format',
'parent-type',
);
protected static function getFieldInfo() {
self::$fields = array(
'author-id' => t('Comment author'),
'body' => t('Comment text. Applying site default input format: will often be wrong'),
'body-format' => t('Comment text input format. No direct mapping to Drupal format defaults'),
'created-at' => t('The follow-up creation timestamp'),
'updated-at' => t('The follow-up latest update timestamp'),
'parent-id' => t('The ticket to which the follow-up applies'),
'parent-type' => t('The follow-up parent type: always Ticket'),
'x-subject' => t('Virtual: from body'),
);
return self::$fields;
}
public function __construct() {
parent::__construct(MigrateGroup::getInstance('Unfuddle'));
$item_xpath = '/account/projects/project/tickets/ticket/comments/comment';
$items_class = new MigrateItemsXML($this->items_url, $item_xpath, $this->item_ID_xpath);
$this->source = new MigrateSourceMultiItems($items_class, self::getFieldInfo());
$source_key = array(
'id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
)
);
$this->dependencies = array('UnfuddlePeople', 'UnfuddleProject', 'UnfuddleTicket');
$this->destination = new MigrateDestinationCommentFollowup();
$this->map = new MigrateSQLMap('unfuddle_ticket_followup',
$source_key,
MigrateDestinationNode::getKeySchema());
$ar1 = drupal_map_assoc($this->unmigratedDestinations);
$ar2 = CaseTrackerMeta::getFields();
$this->unmigratedDestinations = array_keys($ar1 + $ar2); // Note: PID ignored
$this->addXPathFieldMapping('uid', 'author-id')
->sourceMigration('UnfuddlePeople');
$this->addXPathFieldMapping('comment', 'body')
->description(t('Applying site default input format: will often be wrong'));
$this->addXPathFieldMapping('nid', 'parent-id')
->sourceMigration('UnfuddleTicket');
$this->addXPathFieldMapping('timestamp', 'updated-at');
$this->addXPathFieldMapping('subject', 'x-subject')
->description(t('Generated'));
$this->afterConstruct();
}
public function preparerow(stdClass $row) {
$row->xml->{'x-subject'} = strip_tags(node_teaser($row->xml->{'body'}, FILTER_FORMAT_DEFAULT, 40));
// echo "SOURCE " . __FUNCTION__ . PHP_EOL;
// print_r($row->xml);
}
}
/**
* Import ticket data from Unfuddle into CaseTracker project nodes
*/
class UnfuddleTicketMigration extends UnfuddleMigration {
public $unmigratedDestinations = array(
'status',
'promote',
'sticky',
'revision',
'language',
);
public $unmigratedSources = array(
'component-id',
'description-format',
'due-on',
'hours-estimate-current',
'hours-estimate-initial',
'milestone-id',
'resolution',
'resolution-description',
'severity-id',
'version-id',
);
public function __construct() {
parent::__construct(MigrateGroup::getInstance('Unfuddle'));
$item_xpath = '/account/projects/project/tickets/ticket';
$items_class = new MigrateItemsXML($this->items_url, $item_xpath, $this->item_ID_xpath);
$this->source = new MigrateSourceMultiItems($items_class, self::getFieldInfo());
$source_key = array(
'id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
)
);
$this->dependencies = array('UnfuddlePeople', 'UnfuddleProject');
$this->destination = new MigrateDestinationNodeCase();
$this->map = new MigrateSQLMap('unfuddle_ticket',
$source_key,
MigrateDestinationNode::getKeySchema());
$this->addXPathFieldMapping('assign_to', 'assignee-id')
->sourceMigration('UnfuddlePeople');
$this->addXPathFieldMapping('created', 'created-at');
$this->addXPathFieldMapping('body', 'description')
->description(t('Applying site default input format: will often be wrong'));
$this->addXPathFieldMapping('teaser', node_teaser('description', FILTER_FORMAT_DEFAULT))
->description(t('Applying site default input format: will often be wrong'));
$this->addXPathFieldMapping('case_priority_id', 'priority');
$this->addXPathFieldMapping('pid', 'project-id')
->sourceMigration('UnfuddleProject');
$this->addXPathFieldMapping('uid', 'reporter-id')
->sourceMigration('UnfuddlePeople');
$this->addXPathFieldMapping('revision_uid', 'reporter-id')
->sourceMigration('UnfuddlePeople');
$this->addXPathFieldMapping('case_status_id', 'status');
$this->addXPathFieldMapping('case_number', 'number');
$this->addXPathFieldMapping('title', 'summary');
$this->addXPathFieldMapping('path', 'slug'); // slugged
$this->addXPathFieldMapping('changed', 'updated-at');
$this->addFieldMapping('case_type_id', NULL);
$this->afterConstruct();
}
protected static function getFieldInfo() {
self::$fields = array(
'assignee-id' => t('User Id'),
'component-id' => t('The project component (unused)'),
'created-at' => t('Ticket creation timestamp'),
'description' => t('Description'),
'description-format' => t('The ticket body format - does not match Drupal formats'),
'due-on' => t('Timestamp the ticket should be resolved by'),
/*
'fieldn-value-id' => t('The value for custom field n'),
*/
'hours-estimate-current' => t('The current estimate for ticket resolution'),
'hours-estimate-initial' => t('The initial estimate for ticket resolution'),
'milestone-id' => t('The milestone for which resolution of this ticket is due'),
'number' => t('An apparent duplicate of the ticket id'),
'priority' => t('The priority level for the ticket'),
'project-id' => t('The project this ticket belongs to'),
'reporter-id' => t('The user reporting the ticket'),
'resolution' => t('The way the ticket was resolved'),
'resolution-description' => t('Details about the way the ticket was resolved'),
'severity-id' => t('The ticket severity level'),
'status' => t('The ticket status'),
'summary' => t('The ticket summary'),
'updated-at' => t('Timestamp of latest ticket change'),
'version-id' => t('The ticket version'),
'slug' => t('Virtual: ticket summary, converted to slug'),
);
return self::$fields;
}
public function preparerow(stdClass $row) {
/*
$row->xml->{'archived'} = ((int) $row->xml->{'archived'}) ? 0 : 1;
$row->xml->{'short-name'} = 'project/'. $row->xml->{'short-name'};
*/
$row->xml->{'priority'} = 2; // "Normal"
$status = (string) $row->xml->{'status'};
$ct_status_map = array(
'Open' => 4,
'Resolved' => 5,
'Deferred' => 6, // Is not a status in Unfuddle
'Duplicate' => 7, // Is not a status in Unfuddle
'Closed' => 8,
);
$u_status_map = array(
'fixed' => $ct_status_map['Resolved'],
'closed' => $ct_status_map['Closed'],
'reopened' => $ct_status_map['Open'],
'reassigned' => $ct_status_map['Open'],
'new' => $ct_status_map['Open'],
'accepted' => $ct_status_map['Open'],
);
$row->xml->{'status'} = isset($u_status_map[$status])
? $u_status_map[$status]
: $ct_status_map['Open'];
$row->xml->{'slug'} = 'case/'. drupal_strtolower(drupal_clean_css_identifier((string) $row->xml->{'summary'}));
}
}