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'})); } }