UnfuddleMigration.inc 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <?php
  2. /**
  3. * @file
  4. * Migration class to import user accounts from an Unfuddle dump.
  5. *
  6. * @copyright Coyright (c) 2011 Ouest Systèmes Informatiques (OSI, OSInet)
  7. *
  8. * @license Licensed under the General Public License version 2 and later, and the CeCILL 2.0 license.
  9. */
  10. abstract class UnfuddleMigration extends XMLMigration {
  11. public $items_url;
  12. public $item_ID_xpath = 'id';
  13. public $unmigratedDestinations = array();
  14. public $unmigratedSources = array();
  15. protected static $fields = array();
  16. public function __construct($group) {
  17. parent::__construct($group);
  18. $this->items_url = file_directory_path() . '/unfuddle/backup.xml';
  19. }
  20. public function afterConstruct() {
  21. $this->addUnmigratedDestinations($this->unmigratedDestinations, t('White hole'));
  22. $this->addUnmigratedSources($this->unmigratedSources, t('Black hole'));
  23. }
  24. /**
  25. * Return a field-name-indexed hash of field descriptions.
  26. *
  27. * @return array
  28. */
  29. protected static function getFieldInfo() {
  30. return self::$fields;
  31. }
  32. public function addXPathFieldMapping($destination_field, $source_field, $xpath = NULL) {
  33. if (!isset($xpath)) {
  34. $xpath = $source_field;
  35. }
  36. return $this->addFieldMapping($destination_field, $source_field)
  37. ->xpath($xpath);
  38. }
  39. }
  40. /**
  41. * Import "people" data from the Unfuddle backup into Drupal users
  42. */
  43. class UnfuddlePeopleMigration extends UnfuddleMigration {
  44. public $unmigratedDestinations = array(
  45. 'pass',
  46. 'roles',
  47. 'theme',
  48. 'signature',
  49. 'signature_format',
  50. 'language',
  51. 'picture',
  52. );
  53. public $unmigratedSources = array(
  54. 'account-id',
  55. 'first-name',
  56. 'identity-url',
  57. 'is-administrator',
  58. 'last-name',
  59. 'notification-frequency',
  60. 'notification-ignore-self',
  61. 'notification-last-sent',
  62. 'notification-scope-messages',
  63. 'notification-scope-milestones',
  64. 'notification-scope-notebooks',
  65. 'notification-scope-source',
  66. 'notification-scope-tickets',
  67. 'text-markup',
  68. 'updated-at',
  69. );
  70. protected static function getFieldInfo() {
  71. self::$fields = array(
  72. 'account-id' => t('Unfuddle account Id'),
  73. 'id' => t('User account ID'),
  74. 'created-at' => t('User account creation timestamp'),
  75. 'email' => t('Current email address for the account'),
  76. 'first-name' => t('User given name'),
  77. 'identity-url' => t('OpenID URL for the user account'),
  78. 'is-administrator' => t('User is an Unfuddle project administrator'),
  79. 'is-removed' => t('User account has been removed'),
  80. 'last-name' => t('User last name'),
  81. 'last-signed-in' => t('Latest login timestamp'),
  82. 'notification-frequency' => t('Unfuddle notification frequency'),
  83. 'notification-ignore-self' => t('Ignore self when sending notifications'),
  84. 'notification-last-sent' => t('Timestamp of latest notification sent'),
  85. 'notification-scope-messages' => t('Send message notifications'),
  86. 'notification-scope-milestones' => t('Send milestones notifications'),
  87. 'notification-scope-notebooks' => t('Send notebooks notifications'),
  88. 'notification-scope-source' => t('Send source notifications'),
  89. 'notification-scope-tickets' => t('Send tickets notifications'),
  90. 'text-markup' => t('Preferred markup format for issues'),
  91. 'time-zone' => t('Timezone'),
  92. 'updated-at' => t('Timestamp of latest user account change'),
  93. 'username' => t('The user account login name'),
  94. );
  95. return parent::getFieldInfo();
  96. }
  97. public function __construct() {
  98. parent::__construct(MigrateGroup::getInstance('Unfuddle'));
  99. $item_xpath = '/account/people/person';
  100. $items_class = new MigrateItemsXML($this->items_url, $item_xpath, $this->item_ID_xpath);
  101. $this->source = new MigrateSourceMultiItems($items_class, self::getFieldInfo());
  102. $source_key = array(
  103. 'id' => array(
  104. 'type' => 'int',
  105. 'unsigned' => TRUE,
  106. 'not null' => TRUE,
  107. )
  108. );
  109. $this->destination = new MigrateDestinationUser();
  110. $this->map = new MigrateSQLMap('unfuddle_people',
  111. $source_key,
  112. MigrateDestinationUser::getKeySchema());
  113. // Do not map uid: this prevents user_save from creating the accounts.
  114. $this->addFieldMapping('uid', NULL)
  115. ->defaultValue(0);
  116. $this->addXPathFieldMapping('created', 'created-at');
  117. $this->addXPathFieldMapping('mail', 'email');
  118. $this->addXPathFieldMapping('init', 'email')
  119. ->defaultValue('support@osinet.fr')
  120. ->description(t('Unfuddle does not keep the original email address'));
  121. $this->addXPathFieldMapping('status', 'is-removed')
  122. ->description(t('Inverted when converting to user.status'));
  123. $this->addXPathFieldMapping('login', 'last-signed-in');
  124. $this->addXPathFieldMapping('access', 'last-signed-in')
  125. ->description(t('Unfuddle does not log access time, only sign in.'));
  126. $this->addXPathFieldMapping('timezone', 'time-zone')
  127. ->description(t('Only Paris and London currently supported, without DST'));
  128. $this->addXPathFieldMapping('name', 'username');
  129. $this->afterConstruct();
  130. }
  131. /**
  132. * - Timestamp format conversion not needed, as per MigrationBase::timestamp()
  133. * - Invert is-removed to convert to status
  134. * - Convert Unfuddle timezones to Drupal offset seconds
  135. *
  136. * Do not forget type casts when reading SimpleXML elements.
  137. *
  138. * @param object $row
  139. */
  140. public function prepareRow(stdClass $row) {
  141. // Automatic via MigrationBase::timestamp()
  142. // $row->xml->{'created-at'} = strtotime($row->xml->{'created-at'});
  143. // $row->xml->{'last-signed-in'} = strtotime($row->xml->{'last-signed-in'});
  144. $row->xml->{'is-removed'} = ((int) $row->xml->{'is-removed'}) ? 0 : 1;
  145. // @todo TODO Primitive and does not account for DST
  146. $timezones = array(
  147. 'London' => 0,
  148. 'Paris' => 7200,
  149. );
  150. $timezone = (string) $row->xml->{'time-zone'};
  151. $row->xml->{'time-zone'} = empty($timezones[$timezone])
  152. ? 0
  153. : $timezones[$timezone];
  154. }
  155. }
  156. /**
  157. * Import "project" core data from Unfuddle into CaseTracker project nodes
  158. */
  159. class UnfuddleProjectMigration extends UnfuddleMigration {
  160. public $unmigratedDestinations = array(
  161. 'promote',
  162. 'sticky',
  163. 'revision',
  164. 'language',
  165. );
  166. public $unmigratedSources = array(
  167. 'account-id',
  168. 'assignee-on-resolve',
  169. 'backup-frequency',
  170. 'close-ticket-simultaneously-default',
  171. 'default-ticket-report-id',
  172. 'disk-usage',
  173. 'enable-time-tracking',
  174. 's3-access-key-id',
  175. 's3-backup-enabled',
  176. 's3-bucket-name',
  177. 'theme',
  178. 'categories', // @todo TODO Taxonomy ?
  179. );
  180. protected static function getFieldInfo() {
  181. self::$fields = array(
  182. 'account-id' => t('Unfuddle account Id'),
  183. 'archived' => t('Unpublished'),
  184. 'assignee-on-resolve' => t('The user being assigned a ticket once it is resolved'),
  185. 'backup-frequency' => t('Current email address for the account'),
  186. 'close-ticket-simultaneously-default' => t('Close tickets on Resolved status'),
  187. 'created-at' => t('Project creation timestamp'),
  188. 'default-ticket-report-id' => t('User reporting tickets by default'),
  189. 'description' => t('Description'),
  190. 'disk-usage' => t('Disk usage on Unfuddle, in bytes'),
  191. 'enable-time-tracking' => t('Enable issue time tracking'),
  192. 's3-access-key-id' => t('Amazon S3 key'),
  193. 's3-backup-enabled' => t('Periodic backups sent to Amazon S3'),
  194. 's3-bucket-name' => t('Amazon S3 destination bucket for backups'),
  195. 'short-name' => t('Short project name (machine name)'),
  196. 'theme' => t('Unfuddle main color for project theme'),
  197. /**
  198. * @todo TODO insert as CCK fields
  199. *
  200. 'ticket-fieldn-active' => (boolean),
  201. 'ticket-fieldn-disposition' => 'list',
  202. 'ticket-fieldn-title' => (label)
  203. *
  204. */
  205. 'title' => t('Title'),
  206. 'updated-at' => t('Timestamp of latest project change'),
  207. 'categories' => t('Categories'),
  208. /*
  209. * Ignored:
  210. components
  211. custom-field-values
  212. messages
  213. milestones
  214. notebooks
  215. severities
  216. tickets -> separate import
  217. ticket-reports (views)
  218. versions
  219. *
  220. */
  221. );
  222. return self::$fields;
  223. }
  224. public function __construct() {
  225. parent::__construct(MigrateGroup::getInstance('Unfuddle'));
  226. $item_xpath = '/account/projects/project';
  227. $items_class = new MigrateItemsXML($this->items_url, $item_xpath, $this->item_ID_xpath);
  228. $this->source = new MigrateSourceMultiItems($items_class, self::getFieldInfo());
  229. $source_key = array(
  230. 'id' => array(
  231. 'type' => 'int',
  232. 'unsigned' => TRUE,
  233. 'not null' => TRUE,
  234. )
  235. );
  236. $this->dependencies = array('UnfuddlePeople');
  237. $this->destination = new MigrateDestinationNode('casetracker_basic_project');
  238. $this->map = new MigrateSQLMap('unfuddle_project',
  239. $source_key,
  240. MigrateDestinationNode::getKeySchema());
  241. $this->addXPathFieldMapping('created', 'created-at');
  242. $this->addXPathFieldMapping('status', 'archived')
  243. ->description(t('Inverted when converting to node.status'));
  244. $this->addXPathFieldMapping('body', 'description')
  245. ->description(t('Applying site default input format: will often be wrong'));
  246. $this->addXPathFieldMapping('teaser', node_teaser('description', FILTER_FORMAT_DEFAULT))
  247. ->description(t('Applying site default input format: will often be wrong'));
  248. $this->addXPathFieldMapping('changed', 'updated-at');
  249. $this->addXPathFieldMapping('title', 'title');
  250. $this->addXPathFieldMapping('uid', NULL)
  251. ->sourceMigration('UnfuddlePeople')
  252. ->defaultValue(1);
  253. $this->addXPathFieldMapping('revision_uid', NULL)
  254. ->sourceMigration('UnfuddlePeople')
  255. ->defaultValue(1);
  256. $this->addXPathFieldMapping('path', 'short-name')
  257. ->description(t('Auto-aliased to project/(short-name)'));
  258. $this->afterConstruct();
  259. }
  260. public function preparerow(stdClass $row) {
  261. $row->xml->{'archived'} = ((int) $row->xml->{'archived'}) ? 0 : 1;
  262. $row->xml->{'short-name'} = 'project/'. drupal_clean_css_identifier($row->xml->{'short-name'});
  263. }
  264. }
  265. class MigrateDestinationNodeCase extends MigrateDestinationNode {
  266. public function __construct() {
  267. parent::__construct('casetracker_basic_case');
  268. }
  269. /**
  270. * Could also be implemented as a migrate hook_migrate_fields() in module.
  271. * Which is best ?
  272. *
  273. * (non-PHPdoc)
  274. * @see MigrateDestinationNode::fields()
  275. */
  276. public function fields() {
  277. $fields = parent::fields();
  278. $fields += array(
  279. 'pid' => t('Case project'),
  280. 'case_number' => t('Case number'),
  281. 'assign_to' => t('Case assigned to'),
  282. 'case_priority_id' => t('Case priority'),
  283. 'case_type_id' => t('Case type'),
  284. 'case_status_id' => t('Case status'),
  285. );
  286. return $fields;
  287. }
  288. /**
  289. * Casetracker saves its fields in a separate stdClass extra field.
  290. *
  291. * (non-PHPdoc)
  292. * @see MigrateDestinationEntity::prepare()
  293. */
  294. public function prepare($entity, stdClass $source_row) {
  295. $extraFields = array(
  296. 'pid',
  297. 'case_priority_id',
  298. 'case_type_id',
  299. 'assign_to',
  300. 'case_status_id',
  301. );
  302. $entity->casetracker = new stdClass();
  303. foreach ($extraFields as $extra) {
  304. if (isset($entity->$extra)) {
  305. $entity->casetracker->$extra = $entity->$extra;
  306. unset($entity->extra);
  307. }
  308. }
  309. $entity->casetracker->case_type_id = 9; // Bug. Case type is not a default Unfuddle field
  310. }
  311. }
  312. /**
  313. * Import "project" ticket data from Unfuddle into CaseTracker project nodes
  314. */
  315. class UnfuddleTicketMigration extends UnfuddleMigration {
  316. public $unmigratedDestinations = array(
  317. 'status',
  318. 'promote',
  319. 'sticky',
  320. 'revision',
  321. 'language',
  322. );
  323. public $unmigratedSources = array(
  324. 'component-id',
  325. 'description-format',
  326. 'due-on',
  327. 'hours-estimate-current',
  328. 'hours-estimate-initial',
  329. 'milestone-id',
  330. 'resolution',
  331. 'resolution-description',
  332. 'severity-id',
  333. 'version-id',
  334. );
  335. protected static function getFieldInfo() {
  336. self::$fields = array(
  337. 'assignee-id' => t('User Id'),
  338. 'component-id' => t('The project component (unused)'),
  339. 'created-at' => t('Ticket creation timestamp'),
  340. 'description' => t('Description'),
  341. 'description-format' => t('The ticket body format - does not match Drupal formats'),
  342. 'due-on' => t('Timestamp the ticket should be resolved by'),
  343. /*
  344. 'fieldn-value-id' => t('The value for custom field n'),
  345. */
  346. 'hours-estimate-current' => t('The current estimate for ticket resolution'),
  347. 'hours-estimate-initial' => t('The initial estimate for ticket resolution'),
  348. 'milestone-id' => t('The milestone for which resolution of this ticket is due'),
  349. 'number' => t('An apparent duplicate of the ticket id'),
  350. 'priority' => t('The priority level for the ticket'),
  351. 'project-id' => t('The project this ticket belongs to'),
  352. 'reporter-id' => t('The user reporting the ticket'),
  353. 'resolution' => t('The way the ticket was resolved'),
  354. 'resolution-description' => t('Details about the way the ticket was resolved'),
  355. 'severity-id' => t('The ticket severity level'),
  356. 'status' => t('The ticket status'),
  357. 'summary' => t('The ticket summary'),
  358. 'updated-at' => t('Timestamp of latest ticket change'),
  359. 'version-id' => t('The ticket version'),
  360. 'slug' => t('Virtual: ticket summary, converted to slug'),
  361. );
  362. return self::$fields;
  363. }
  364. public function __construct() {
  365. parent::__construct(MigrateGroup::getInstance('Unfuddle'));
  366. $item_xpath = '/account/projects/project/tickets/ticket';
  367. $items_class = new MigrateItemsXML($this->items_url, $item_xpath, $this->item_ID_xpath);
  368. $this->source = new MigrateSourceMultiItems($items_class, self::getFieldInfo());
  369. $source_key = array(
  370. 'id' => array(
  371. 'type' => 'int',
  372. 'unsigned' => TRUE,
  373. 'not null' => TRUE,
  374. )
  375. );
  376. $this->dependencies = array('UnfuddlePeople', 'UnfuddleProject');
  377. $this->destination = new MigrateDestinationNodeCase();
  378. $this->map = new MigrateSQLMap('unfuddle_ticket',
  379. $source_key,
  380. MigrateDestinationNode::getKeySchema());
  381. $this->addXPathFieldMapping('assign_to', 'assignee-id')
  382. ->sourceMigration('UnfuddlePeople');
  383. $this->addXPathFieldMapping('created', 'created-at');
  384. $this->addXPathFieldMapping('body', 'description')
  385. ->description(t('Applying site default input format: will often be wrong'));
  386. $this->addXPathFieldMapping('teaser', node_teaser('description', FILTER_FORMAT_DEFAULT))
  387. ->description(t('Applying site default input format: will often be wrong'));
  388. $this->addXPathFieldMapping('case_priority_id', 'priority');
  389. $this->addXPathFieldMapping('pid', 'project-id')
  390. ->sourceMigration('UnfuddleProject');
  391. $this->addXPathFieldMapping('uid', 'reporter-id')
  392. ->sourceMigration('UnfuddlePeople');
  393. $this->addXPathFieldMapping('revision_uid', 'reporter-id')
  394. ->sourceMigration('UnfuddlePeople');
  395. $this->addXPathFieldMapping('case_status_id', 'status');
  396. $this->addXPathFieldMapping('case_number', 'number');
  397. $this->addXPathFieldMapping('title', 'summary');
  398. $this->addXPathFieldMapping('path', 'slug'); // slugged
  399. $this->addXPathFieldMapping('changed', 'updated-at');
  400. $this->addFieldMapping('case_type_id', NULL);
  401. $this->afterConstruct();
  402. }
  403. public function preparerow(stdClass $row) {
  404. /*
  405. $row->xml->{'archived'} = ((int) $row->xml->{'archived'}) ? 0 : 1;
  406. $row->xml->{'short-name'} = 'project/'. $row->xml->{'short-name'};
  407. */
  408. $row->xml->{'priority'} = 2; // "Normal"
  409. $status = (string) $row->xml->{'status'};
  410. $ct_status_map = array(
  411. 'Open' => 4,
  412. 'Resolved' => 5,
  413. 'Deferred' => 6, // Is not a status in Unfuddle
  414. 'Duplicate' => 7, // Is not a status in Unfuddle
  415. 'Closed' => 8,
  416. );
  417. $u_status_map = array(
  418. 'fixed' => $ct_status_map['Resolved'],
  419. 'closed' => $ct_status_map['Closed'],
  420. 'reopened' => $ct_status_map['Open'],
  421. 'reassigned' => $ct_status_map['Open'],
  422. 'new' => $ct_status_map['Open'],
  423. 'accepted' => $ct_status_map['Open'],
  424. );
  425. $row->xml->{'status'} = isset($u_status_map[$status])
  426. ? $u_status_map[$status]
  427. : $ct_status_map['Open'];
  428. $row->xml->{'slug'} = 'case/'. drupal_strtolower(drupal_clean_css_identifier((string) $row->xml->{'summary'}));
  429. }
  430. }