php8
[web.mtrack] / admin / importcsv.php
1 <?php # vim:ts=2:sw=2:et:
2 include '../../inc/common.php';
3
4 MTrackACL::requireAllRights("Tickets", 'create');
5 session_start();
6
7 $field_aliases = array(
8   'state' => 'status',
9   'pri' => 'priority',
10   'id' => 'ticket',
11   'type' => 'classification',
12 );
13 $supported_fields = array(
14   'classification',
15   'ticket',
16   'milestone',
17   '-milestone',
18   '+milestone',
19   'summary',
20   'status',
21   'priority',
22   'owner',
23   'type',
24   'component',
25   '-component',
26   '+component',
27   'description'
28 );
29 foreach ($supported_fields as $i => $f) {
30   unset($supported_fields[$i]);
31   $supported_fields[$f] = $f;
32 }
33
34 $C = MTrackTicket_CustomFields::getInstance();
35 foreach ($C->fields as $f) {
36   $name = substr($f->name, 2);
37   $supported_fields[$f->name] = $f->name;
38   if (!isset($field_aliases[$name])) {
39     $field_aliases[$name] = $f->name;
40   }
41 }
42
43 if ($_SERVER['REQUEST_METHOD'] == 'POST') {
44
45   if (isset($_FILES['csvfile']) && $_FILES['csvfile']['error'] == 0
46       && is_uploaded_file($_FILES['csvfile']['tmp_name'])) {
47     ini_set('auto_detect_line_endings', true);
48     $fp = fopen($_FILES['csvfile']['tmp_name'], 'r');
49     $header = fgetcsv($fp);
50     $err = array();
51     $output = array();
52     foreach ($header as $i => $name) {
53       $name = strtolower($name);
54       if (isset($field_aliases[$name])) {
55         $name = $field_aliases[$name];
56       }
57       if (!isset($supported_fields[$name])) {
58         $err[] = "Unsupported field: $name";
59       }
60       $header[$i] = $name;
61     }
62     $db = MTrackDB::get();
63     $db->beginTransaction();
64     MTrackChangeset::$use_txn = false;
65     $todo = array();
66     do {
67       $line = fgetcsv($fp);
68       if ($line === false) break;
69
70       $item = array();
71       foreach ($header as $i => $name) {
72         $item[$name] = $line[$i];
73       }
74
75       if (isset($item['ticket'])) {
76         $id = $item['ticket'];
77         if ($id[0] == '#') {
78           $id = substr($id, 1);
79         }
80         try {
81           $tkt = MTrackIssue::loadByNSIdent($id);
82           if ($tkt == null) {
83             $err[] = "No such ticket $id";
84             continue;
85           }
86         } catch (Exception $e) {
87           $err[] = $e->getMessage();
88           continue;
89         }
90         $output[] = "<b>Updating ticket $tkt->nsident</b><br>\n";
91       } else {
92         $tkt = new MTrackIssue;
93         $tkt->priority = 'normal';
94         list($tkt->nsident) = MTrackDB::q(
95           'select max(cast(nsident as integer)) + 1 from tickets')
96           ->fetchAll(PDO::FETCH_COLUMN, 0);
97         if ($tkt->nsident === null) {
98           $tkt->nsident = 1;
99         }
100         $output[] = "<b>Creating ticket $tkt->nsident<b><br>\n";
101       }
102       $CS = MTrackChangeset::begin("ticket:X", $_POST['comment']);
103       if (strlen(trim($_POST['comment']))) {
104         $tkt->addComment($_POST['comment']);
105       }
106       foreach ($item as $name => $value) {
107         if ($name == 'ticket') {
108           continue;
109         }
110         $output[] = "$name => $value<br>\n";
111         try {
112           switch ($name) {
113             case 'summary':
114             case 'description':
115             case 'classification':
116             case 'priority':
117             case 'severity':
118             case 'changelog':
119             case 'owner':
120             case 'cc':
121               $tkt->$name = strlen($value) ? $value : null;
122               break;
123             case 'milestone':
124               if (strlen($value)) {
125                 foreach ($tkt->getMilestones() as $mid) {
126                   $tkt->dissocMilestone($mid);
127                 }
128                 $tkt->assocMilestone($value);
129               }
130               break;
131             case '+milestone':
132               if (strlen($value)) {
133                 $tkt->assocMilestone($value);
134               }
135               break;
136             case '-milestone':
137               if (strlen($value)) {
138                 $tkt->dissocMilestone($value);
139               }
140               break;
141             case 'component':
142               if (strlen($value)) {
143                 foreach ($tkt->getComponents() as $mid) {
144                   $tkt->dissocComponent($mid);
145                 }
146                 $tkt->assocComponent($value);
147               }
148               break;
149             case '+component':
150               if (strlen($value)) {
151                 $tkt->assocComponent($value);
152               }
153               break;
154             case '-component':
155               if (strlen($value)) {
156                 $tkt->dissocComponent($value);
157               }
158               break;
159             default:
160               if (!strncmp($name, 'x_', 2)) {
161                 $tkt->{$name} = $value;
162               }
163               break;
164           }
165         } catch (Exception $e) {
166           $err[] = $e->getMessage();
167         }
168       }
169       $tkt->save($CS);
170       $CS->setObject("ticket:" . $tkt->tid);
171
172     } while (true);
173     $_SESSION['admin.import.result'] = array($err, $output);
174     if (count($err)) {
175       $db->rollback();
176     } else {
177       $db->commit();
178     }
179   }
180   header("Location: {$ABSWEB}admin/importcsv.php");
181   exit;
182 }
183
184 if (isset($_SESSION['admin.import.result'])) {
185   list($err, $info) = $_SESSION['admin.import.result'];
186   unset($_SESSION['admin.import.result']);
187
188   mtrack_head(count($err) ? 'Import Failed' : 'Import Complete');
189
190   foreach ($info as $line) {
191     echo $line;
192   }
193
194   if (count($err)) {
195     echo "The following errors were encountered:<br>\n";
196     foreach ($err as $msg) {
197       echo htmlentities($msg) . "<br>\n";
198     }
199     echo "<br><b>No changes were committed</b><br>\n";
200   } else {
201     echo "<br><b>Done!</b>\n";
202   }
203
204   mtrack_foot();
205   exit;
206 }
207
208 mtrack_head('Import');
209
210 ?>
211 <h1>Import/Update via CSV</h1>
212
213 <p>
214 You may use this facility to change ticket properties en-masse by uploading
215 a CSV file.
216 </p>
217
218 <ul>
219   <li>If a ticket column is present and non-empty,
220     that ticket will be updated</li>
221   <li>If there is no ticket column, or the ticket column is empty,
222     then a ticket will be created</li>
223   <li>If any errors are detected, none of the changes from the CSV file
224     will be applied</li>
225 </ul>
226
227 <p>
228 The input file must be a CSV file with the field names on the first line.
229 </p>
230
231 <p>
232 The following fields are supported:
233 </p>
234
235 <dl>
236   <dt>ticket</dt>
237   <dd>The ticket number</dd>
238
239   <dt>milestone</dt>
240   <dd>The value to use for the milestone.  If updating an existing ticket,
241    this field will remove any other milestones in the ticket and set it to
242    only this value.
243   </dd>
244
245   <dt>-milestone</dt>
246   <dd>Removes a milestone; if the ticket is associated with the named milestone,
247    it will be removed from that milestone.
248   </dd>
249
250   <dt>+milestone</dt>
251   <dd>Associates the ticket with the named milestone, preserving any other
252   milestones currently associated with the ticket.
253   </dd>
254
255   <dt>summary</dt>
256   <dd>Sets the summary for the ticket</dd>
257
258   <dt>status or state</dt>
259   <dd>Sets the state of the ticket; can be one of the configured ticket states
260   </dd>
261
262   <dt>priority</dt>
263   <dd>Sets the priority; can be one of the configured priorities</dd>
264
265   <dt>owner</dt>
266   <dd>Sets the owner</dd>
267
268   <dt>type</dt>
269   <dd>Sets the ticket type</dd>
270
271   <dt>component</dt>
272   <dd>Sets the component, replacing all other component associations</dd>
273
274   <dt>-component</dt>
275   <dd>Removes association with the named component</dd>
276
277   <dt>+component</dt>
278   <dd>Associates with the named component, preserving existing associations</dd>
279
280   <dt>description</dt>
281   <dd>Sets the description of the ticket</dd>
282
283 <?php
284
285 foreach ($C->fields as $f) {
286   $name = substr($f->name, 2);
287   if (!isset($field_aliases[$name]) || $field_aliases[$name] != $f->name) {
288     $name = $f->name;
289     echo "<dt>$name</dt>\n";
290   } else {
291     echo "<dt>$name</dt>\n";
292     echo "<dt>$f->name</dt>\n";
293   }
294   echo "<dd>" . htmlentities($f->label, ENT_QUOTES, 'utf-8') . "\n";
295
296   if ($f->type == 'select') {
297     echo "<br>Value may be one of:<br>";
298     $data = $f->ticketData();
299     foreach ($data['options'] as $opt) {
300       echo " <tt>" . htmlentities($opt, ENT_QUOTES, 'utf-8') . "</tt><br>";
301     }
302   }
303
304   echo "</dd>\n";
305 }
306
307 ?>
308
309 </dl>
310
311 <h2>Import</h2>
312
313 <p>Enter a comment in the box below; it will be added as a comment to
314 all affected tickets</p>
315
316 <form method='post' enctype='multipart/form-data'>
317   <textarea name='comment' id='comment'
318     class='code wiki' rows='4' cols='78'></textarea>
319   <input type='file' name='csvfile'>
320   <input type='submit' value='Import'>
321 </form>
322
323 <?php
324 mtrack_foot();
325