PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/README.md

https://github.com/jwcastillo/RedmineIssueImporter
Markdown | 475 lines | 414 code | 61 blank | 0 comment | 0 complexity | 87f9c6bf92d04f1b71fc199e516ba4f6 MD5 | raw file
  1. RedmineIssueImporter
  2. ====================
  3. Use the Command Line and a config yml File to import CVS Sheets as Issues in Redmine. *With Custom Fields support*. And easily mapping your CSV different/corrupt fields values to the values accepted by Redmine (lists, boolean, date, etc..) fields
  4. The Import process:
  5. -------------------
  6. 1) Identify the fields in the CVS sheet that match fields in Redmine Issues.
  7. 2) Add connection info to the config file following the example.
  8. 3) Add Custom fields info into the config file, name and id, to allow importer to work properly.
  9. 4) add mapping info, relating fields in sheet with Redmine fields.
  10. 5) Fill Issue default values to use for the issues when the sheet field comes empty or when there is
  11. no such field in sheet but redmine forces you to use it.
  12. 6) Run the application from console.
  13. 7) Optionally delete created Issues if mappings should be fixed.
  14. The Update Process:
  15. -------------------
  16. 1 & 2 & 3 & 4 from above Import Process
  17. 5) As oposite to Import, only use default values for fields present in issues sheet. Otherwise you'll override current values in existing issues with their defaults.
  18. 6) Run Application from Comman Line. e.g. $ php importSheet.php update /tmp/updateDemandas.csv --sheet=updatedemandas --record=demanda
  19. In this example I just used issues Id from /tmp/updateDemandas.csv and set a default value for a new Field (field added after Import). @see Config/Consig.yml
  20. You have three commands:
  21. ```
  22. import --sheet="..." --record="..." [--delimiter="..."] [--fileType="..."] dataFile
  23. update --sheet="..." --record="..." [--delimiter="..."] [--fileType="..."] dataFile
  24. delete [--all] --project="..."
  25. $ php importSheet.php list
  26. ..
  27. Available commands:
  28. delete Deletes all issues (only if --all) in a project or issued created in last import command run. Does nothing if cant find serielized created issues ids file
  29. import Imports issues in a Redmine project, from a data sheet
  30. update update issues in a Redmine project, from a data sheet, given that Config/Config.yml denotes the "id" field in sheet
  31. $ php importSheet.php help import
  32. Usage:
  33. import [--sheet="..."] [--record="..."] [--delimiter="..."] [--fileType="..."] dataFile
  34. Arguments:
  35. dataFile absolute file path to the data sheet
  36. Options:
  37. --sheet The sheet name config to be used to interpret data in input sheet file.
  38. --record The default record name config to be used to interpret data in input sheet file.
  39. --delimiter The sheet field delimiter, for CSV.
  40. --fileType The default file Format to be used, its case sensitive as the parser class name beggins with this string and ends with 'SheetRecordParser' i.e. 'CsvSheetRecordParser'. (default: "Csv")
  41. $ php importSheet.php help update
  42. Usage:
  43. update [--sheet="..."] [--record="..."] [--delimiter="..."] [--fileType="..."] dataFile
  44. Arguments:
  45. dataFile absolute file path to the data sheet
  46. Options:
  47. --sheet The sheet name config to be used to interpret data in input sheet file.
  48. --record The default record name config to be used to interpret data in input sheet file.
  49. --delimiter The sheet field delimiter, for CSV.
  50. --fileType The default file Format to be used, its case sensitive as the parser class name beggins with this string and ends with 'SheetRecordParser' i.e. 'CsvSheetRecordParser'. (default: "Csv")
  51. $ php importSheet.php help delete
  52. Usage:
  53. delete [--all] [--project="..."]
  54. Options:
  55. --all if Specified, deletes all issues in a project. WARNING! all issues, not just the imported ones.
  56. --project This must be set either if --all is set or just last Run Ids need to be deleted. Its the project identifier, the one that appears in the URL. i.e. <RedmineDomain>/projects/<projectIdentifier>/issues?...
  57. ```
  58. The Config File
  59. ---------------
  60. Use the following config file format to import sheets into redmine. So far it creates Issues,
  61. with custom fields, but it's extensible to add users, etc..
  62. ```yaml
  63. ################################################################################
  64. # Run customization
  65. ################################################################################
  66. redmine_account:
  67. # http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication # show API Key.
  68. api_key: 'fc..8' # siup
  69. host: 'http://redmine.myserver.com.ar/'
  70. input_format:
  71. file_type: 'Csv' # maps to CsvSheetRecordParser
  72. delimiter: '#' # field delimiter
  73. # false Makes everithing but call save on entities, for tests before hitting API
  74. save_records: true
  75. ################################################################################
  76. # Sheets mapping configuration
  77. ################################################################################
  78. on_error:
  79. behavior: continue # [continue, stop, rollback]
  80. display_exceptions: false # [true | false]
  81. # redmine custom fields settings <objectType> => <customFieldName> => id => <idValue>
  82. custom_fields:
  83. issue:
  84. sprint:
  85. id: 1 # for issues, Intermediario field
  86. Localidad:
  87. id: 19 # for issues, Localidad field
  88. "Fecha Comprometido":
  89. id: 21 # for issues, Fecha Comprometido field
  90. Telefono:
  91. id: 20 # for issues, Telefono field
  92. Beneficiario:
  93. id: 18 # for issues, Beneficiario field
  94. Intermediario:
  95. id: 17 # for issues, Intermediario field
  96. # all sheets that can be importes should be mapped here
  97. sheets:
  98. #1st sheet definition
  99. demandas:
  100. # sheet Name, not used unless you add a web form that might use this name.
  101. name: Planilla de Relevamiento de Demandas Area de Nuevos Medios
  102. # each sheet could host several record types, here's each definition
  103. records:
  104. # recordName use as index to select this config in [Run customization]
  105. demanda:
  106. # record Label, not used.
  107. name: Demanda
  108. # Redmine Objects/Entities that are related to sheet's data.
  109. entities:
  110. # Issue object
  111. Issue:
  112. # previously relatd to Doctrien 1.X adapted to Redmine issue types.
  113. # Not used schema_entity: ~ # si entities[entName] no coincide con el esquema setear este valor
  114. # object deefault values (in this case Issue) can be [callbackClass, Callbackmethod]
  115. # these default need not to be in the sheet, but might be mandatory for the API.
  116. # you could also use defaults for cusotm fields here, just with the name.
  117. defaults:
  118. project: A Nuevos Medios, Seguimiento de Demandas
  119. status: Nueva
  120. priority: Normal
  121. assigned_to_id: 92
  122. author: 'juan'
  123. due_date: ~
  124. start_date: [Defaults\Defaults, startDate]
  125. tracker: Demanda
  126. "Fecha Comprometido": '2013-11-01'
  127. Beneficiario: N/N
  128. subject: 'Sin Asunto'
  129. Intermediario: '-----------------'
  130. # These are the fields expected to be present in the CSV or any other sheet like input
  131. # next definition matched a CSV as follows (showing two records):
  132. # [Subject;Description;Sprint] the rest of mandatory values are defined above for "Issue"
  133. # subject1;Description1;8
  134. # subject2;Description2;8
  135. fields:
  136. # field name
  137. beneficiario:
  138. # this key relates this field with Redmine's model described above
  139. model: {entity: 'Issue', column: 'Beneficiario'}
  140. # This are the coordinates, where the parser tries to find the 1st occurrence of the field.
  141. # Zero based. from the upper-left corner
  142. coord: {x: 1, y: 0}
  143. # default value for this field. ovverides other defaults possibly defined above.
  144. default: ~ # Si !== ~ pisa al default del schema y al default en [entities]
  145. # necesary moves to reach next instance of this field, e.i. next record. Normally it'll be just
  146. # one step down. (Y+1, X+0). But for values in headers, that appears only once, might be
  147. # (Y+0, X+0) so, the same value is used for every record.
  148. increment: {x: 0, y: 1}
  149. # callback that might be needed to transform input data before being persisted.
  150. transform: ~ # a callback method
  151. subject:
  152. model: {entity: 'Issue', column: 'subject'} # entity referencia entities[entity] no al schema. Para eso está entities[entity][schema_entity], en caso de que difiera.
  153. coord: {x: 3, y: 0}
  154. default: ~ # Si !== ~ pisa al default del schema y al default en [entities]
  155. increment: {x: 0, y: 1} # ~ = {x: 0, y: 0} if field is recurrent increment determines the relative loction of the next sibling. ~ means the field is no recurrent, only appears once in a sheet-
  156. transform: [Transformers\Transformer, asunto] # a callback method
  157. intermediario:
  158. model: {entity: 'Issue', column: 'Intermediario'} # entity referencia entities[entity] no al schema. Para eso está entities[entity][schema_entity], en caso de que difiera.
  159. coord: {x: 2, y: 0}
  160. default: ~ # Si !== ~ pisa al default del schema y al default en [entities]
  161. increment: {x: 0, y: 1} # ~ = {x: 0, y: 0} if field is recurrent increment determines the relative loction of the next sibling. ~ means the field is no recurrent, only appears once in a sheet-
  162. transform: [Transformers\Transformer, intermediario] # a callback method
  163. description:
  164. model: {entity: 'Issue', column: 'description', glue: '| '} # entity referencia entities[entity] no al schema. Para eso está entities[entity][schema_entity], en caso de que difiera.
  165. coord: {x: 3, y: 0}
  166. default: ~ # Si !== ~ pisa al default del schema y al default en [entities]
  167. increment: {x: 0, y: 1} # ~ = {x: 0, y: 0} if field is recurrent increment determines the relative loction of the next sibling. ~ means the field is no recurrent, only appears once in a sheet-
  168. transform: ~ # a callback method
  169. localidad:
  170. model: { entity: 'Issue', column: 'Localidad'} # entity referencia entities[entity] no al schema. Para eso está entities[entity][schema_entity], en caso de que difiera.
  171. coord: {x: 0, y: 0}
  172. default: ~ # Si !== ~ pisa al default del schema y al default en [entities]
  173. increment: {x: 0, y: 1} # ~ = {x: 0, y: 0} if field is recurrent increment determines the relative loction of the next sibling. ~ means the field is no recurrent, only appears once in a sheet-
  174. transform: [Transformers\Transformer, localidad] # a callback method
  175. estado:
  176. model: { entity: 'Issue', column: 'status'} # entity referencia entities[entity] no al schema. Para eso está entities[entity][schema_entity], en caso de que difiera.
  177. coord: {x: 4, y: 0}
  178. default: ~ # Si !== ~ pisa al default del schema y al default en [entities]
  179. increment: {x: 0, y: 1} # ~ = {x: 0, y: 0} if field is recurrent increment determines the relative loction of the next sibling. ~ means the field is no recurrent, only appears once in a sheet-
  180. transform: [Transformers\Transformer, estado] # a callback method
  181. observaciones:
  182. model: { entity: 'Issue', column: 'description', glue: '| '} # entity referencia entities[entity] no al schema. Para eso está entities[entity][schema_entity], en caso de que difiera.
  183. coord: {x: 5, y: 0}
  184. default: ~ # Si !== ~ pisa al default del schema y al default en [entities]
  185. increment: {x: 0, y: 1} # ~ = {x: 0, y: 0} if field is recurrent increment determines the relative loction of the next sibling. ~ means the field is no recurrent, only appears once in a sheet-
  186. transform: ~ # a callback method
  187. fecha_inicio:
  188. model: { entity: 'Issue', column: 'start_date'} # entity referencia entities[entity] no al schema. Para eso está entities[entity][schema_entity], en caso de que difiera.
  189. coord: {x: 6, y: 0}
  190. default: ~ # Si !== ~ pisa al default del schema y al default en [entities]
  191. increment: {x: 0, y: 1} # ~ = {x: 0, y: 0} if field is recurrent increment determines the relative loction of the next sibling. ~ means the field is no recurrent, only appears once in a sheet-
  192. transform: [Transformers\Transformer, fecha] # a callback method
  193. localidad_en_descripcion:
  194. model: { entity: 'Issue', column: 'description', glue: '| '} # entity referencia entities[entity] no al schema. Para eso está entities[entity][schema_entity], en caso de que difiera.
  195. coord: {x: 0, y: 0}
  196. default: ~ # Si !== ~ pisa al default del schema y al default en [entities]
  197. increment: {x: 0, y: 1} # ~ = {x: 0, y: 0} if field is recurrent increment determines the relative loction of the next sibling. ~ means the field is no recurrent, only appears once in a sheet-
  198. transform: ~ # a callback method
  199. #2st sheet definition
  200. iprodich:
  201. # sheet Name, not used unless you add a web form that might use this name.
  202. name: iprodich
  203. # each sheet could host several record types, here's each definition
  204. records:
  205. # recordName use as index to select this config in [Run customization]
  206. iprodich:
  207. # record Label, not used.
  208. name: iprodich
  209. # Redmine Objects/Entities that are related to sheet's data.
  210. entities:
  211. # Issue object
  212. Issue:
  213. # previously relatd to Doctrien 1.X adapted to Redmine issue types.
  214. # Not used schema_entity: ~ # si entities[entName] no coincide con el esquema setear este valor
  215. # object deefault values (in this case Issue) can be [callbackClass, Callbackmethod]
  216. # these default need not to be in the sheet, but might be mandatory for the API.
  217. # you could also use defaults for cusotm fields here, just with the name.
  218. defaults:
  219. project: Gestión I.Pro.Di.Ch
  220. status: Nueva
  221. priority: Normal
  222. assigned_to: juanmf
  223. author: 'juan'
  224. due_date: ~
  225. start_date: [Defaults\Defaults, startDate]
  226. tracker: Demanda
  227. "Fecha Comprometido": '2013-11-01'
  228. subject: 'Sin Asunto'
  229. sprint: 8
  230. # These are the fields expected to be present in the CSV or any other sheet like input
  231. # next definition matched a CSV as follows (showing two records):
  232. # [Subject] the rest of mandatory values are defined above for "Issue"
  233. # subject1
  234. fields:
  235. # field name
  236. subject:
  237. model: {entity: 'Issue', column: 'subject'} # entity referencia entities[entity] no al schema. Para eso está entities[entity][schema_entity], en caso de que difiera.
  238. coord: {x: 0, y: 0}
  239. default: ~ # Si !== ~ pisa al default del schema y al default en [entities]
  240. increment: {x: 0, y: 1} # ~ = {x: 0, y: 0} if field is recurrent increment determines the relative loction of the next sibling. ~ means the field is no recurrent, only appears once in a sheet-
  241. transform: [Transformers\Transformer, asunto] # a callback method
  242. #3rd sheet definition for updates
  243. updatedemandas:
  244. # sheet Name, not used unless you add a web form that might use this name.
  245. name: Las demandas que se exportaron por algo (enviar a evelyn) y se desea actualizar en batch
  246. # each sheet could host several record types, here's each definition
  247. records:
  248. # recordName use as index to select this config in [Run customization]
  249. demanda:
  250. # record Label, not used.
  251. name: Demanda
  252. project_id: demandas
  253. entities:
  254. # Issue object
  255. Issue:
  256. defaults:
  257. project: A Nuevos Medios, Seguimiento de Demandas
  258. # just adding a boolean true for boolean Field "Derivado"
  259. Derivado: 1
  260. # These are the fields expected to be present in the CSV or any other sheet like input
  261. # next definition matched a CSV as follows (showing two records):
  262. # [id] the rest of mandatory values are defined above for "Issue"
  263. # id1
  264. # id2
  265. fields:
  266. # field name
  267. id:
  268. # this key relates this field with Redmine's model described above
  269. model: {entity: 'Issue', column: 'id'}
  270. # This are the coordinates, where the parser tries to find the 1st occurrence of the id ield.
  271. # Zero based. from the upper-left corner I use y:1 as the y:0 is used by Redmine field name headings
  272. # because I used a CSV resulting from a Redmine Export for the update.
  273. coord: {x: 0, y: 1}
  274. default: ~
  275. increment: {x: 0, y: 1}
  276. transform: ~ # a callback method
  277. ```
  278. config Process & config File filling
  279. ------------------------------------
  280. You complete each part of this config file in each step of the definition process explained above as follows
  281. 1) Identify the fields in the CVS sheet that match fields in Redmine Issues.
  282. Let say we have the following csv record and its the 1st record of the file:
  283. ```
  284. The kid is not my child;Father claims he's not the father;2013-10-05;Waiting for Revision;John Doe
  285. ^ Subject ^ description ^ stdate ^ status ^ relative
  286. ^ coord: {x:0, y:0} ^ coord: {x:1, y:0} ^ {2, 0} ^ {x:3, y:0} ^ {x:4, y:0} # see coord is in fields: {<fieldName>: {coord: {x:, y:}}}
  287. ```
  288. 2) Add connection info to the config file following the example.
  289. fill
  290. ```
  291. redmine_account:
  292. api_key: 'fc..8' # siup
  293. host: http://redmine...
  294. ```
  295. 3) Add Custom fields info into the config file, name and id, to allow importer to work properly.
  296. In this case relative is a text custom field
  297. ```
  298. custom_fields:
  299. issue:
  300. relative:
  301. id: 1
  302. ```
  303. This is necessary so the importer can add the right id value in the REST request sent to Redmine API.
  304. 4) add mapping info, relating fields in sheet with Redmine fields.
  305. fill, for each sheet field of interest (note you don't HAVE to use all fields in input sheet)
  306. the following structure in config file:
  307. ```
  308. subject:
  309. model: {entity: 'Issue', column: 'subject'}
  310. coord: {x: 3, y: 0}
  311. default: ~
  312. increment: {x: 0, y: 1}
  313. transform: [Transformers\Transformer, asunto]
  314. ```
  315. Where subject: is the sheet field name, name it as you wish.
  316. Where model refers to the Redmine entity and column/field name either native or custom
  317. where coord is the position, zero based, of the field in the record. as shown above
  318. where default is the value or function return value that replaces empty sheet cell values.
  319. where increment is the step that the parser must make to find the next field of same type. i.e.
  320. the subject of the next record.
  321. where transform is the function that the entity populator calls to replace the non-empty
  322. values that are found in the subject fields. This is useful for transforming despair human written
  323. values to Redmine List fields or dates. @see Transformers\Transformer. to avoid transformers use ~
  324. 5) Fill Issue default values to use for the issues when the sheet field comes empty or when there is
  325. no such field in sheet but redmine forces you to use it.
  326. ```
  327. sheets:
  328. demandas:
  329. records:
  330. demanda:
  331. name: Demanda
  332. entities:
  333. Issue:
  334. defaults:
  335. project: A Nuevos Medios, Seguimiento de Demandas
  336. status: Nueva
  337. priority: Normal
  338. assigned_to: 'juan'
  339. author: 'juan'
  340. due_date: ~
  341. start_date: [Defaults\Defaults, startDate]
  342. tracker: Demanda
  343. "Fecha Comprometido": '2013-11-01' #custom field
  344. Beneficiario: N/N #custom field
  345. subject: 'Sin Asunto'
  346. Intermediario: '-----------------' #custom field
  347. ```
  348. 6) Run the application from console.
  349. So far we already have a working config, try your 1st import:
  350. ```
  351. import --sheet="..." --record="..." [--delimiter="..."] [--fileType="..."] dataFile
  352. ```
  353. if it worked out just how you wanted, you are done.
  354. 7) Optionally delete created Issues if mappings should be fixed.
  355. If there were errors, check your mappings, transformations, defaults, project name and identifier.
  356. ```
  357. delete [--all] --project="projectIdentifier"
  358. ```
  359. WARNING
  360. if --all is specified, this comand will fetch all issue ids in project and delete them
  361. if you don't specify --all, the application will try to find the lastIssuesId.serialized
  362. that get written every time we finish an import process without a system crash, and holds the
  363. ids under the project identifier. And it will deletes all ids present in it.
  364. Dependencies
  365. ------------
  366. Depends on @see composer.json
  367. [kbsali/php-redmine-api](https://github.com/kbsali/php-redmine-api)
  368. [symfony/yaml](https://github.com/symfony/Yaml)
  369. [symfony/console](https://github.com/symfony/Console)
  370. Instalation
  371. -----------
  372. 1) git clone this repo on your directory of choise
  373. 2) install [Composer](http://getcomposer.org/doc/00-intro.md#installation-windows)
  374. 3) run [composer install](http://getcomposer.org/doc/00-intro.md#using-composer) in this application directory so Composer fills the vendor directory with dependencies
  375. Usage
  376. -----
  377. ```
  378. php importSheet.php help import
  379. php importSheet.php help delete
  380. php importSheet.php import --sheet="sheetNameInConfig" --record="recordNameInConfig" [--delimiter="..."] [--fileType="..."] dataFile
  381. php importSheet.php delete [--all] --project="projectIdentifier"
  382. ```
  383. Example:
  384. ```
  385. php importSheet.php import /home/juanmf/newIssuesCreatedInExcel.csv --sheet=demandas --record=demanda
  386. ```
  387. ups, forgot to add default value for start_date, edit config File [Config/config.yml]:
  388. ```
  389. php importSheet.php delete --project=projectId
  390. php importSheet.php import /home/juanmf/newIssuesCreatedInExcel.csv --sheet=demandas --record=demanda
  391. ```
  392. Where projectId is the project identifier of the project for wich sheet "demandas" was configured.
  393. If you are importing issues to an empty project, for 1st time, then its safe to add --all option to delete command.
  394. Extending It
  395. ------------
  396. So far I just play with Issues [EntityPopulator/Entities/Issue.php](/EntityPopulator/Entities/Issue.php). In [EntityPopulator/Entities](/EntityPopulator/Entities) you can add any Redmine entity, as a class sharing its name and extending "Entity" implement the save method:
  397. Here's the Issue::save(), it's basically a proxy for the real [kbsali/php-redmine-api](https://github.com/kbsali/php-redmine-api) that adapt the field/values of custom fields before sending the create request, and takes care of updates if Id field already exists.
  398. ```php
  399. public function save()
  400. {
  401. parent::adaptCustomFields($this);
  402. if (isset($this['id'])) {
  403. return $this->update();
  404. }
  405. $importService = \ImportService::getInstance();
  406. $api = $importService->getClient()->api(self::API);
  407. /* @var $api \Redmine\Api\Issue */
  408. $return = $api->create($this->toArray());
  409. $this->checkErrors($return);
  410. $this->addIdToCreatedIds($return, $importService);
  411. }
  412. ```
  413. You are welcome to implement any entity (Users, Projects, etc.) from the Redmine [API](http://www.redmine.org/projects/redmine/wiki/Rest_api)
  414. then all you need to do is adapt the [Config/Config.yml] to map your CSV to the right Entities fields (one sheet could map to several Entities at ones).