1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
4 //require_once 'Auth.php';
5 require_once 'MTrack/Repo.php';
6 require_once 'MTrack/DB.php';
7 require_once 'MTrack/Project.php';
8 require_once 'MTrack/SCMEvent.php';
16 static $possible_event_types = array();
17 static $media = array(
19 // 'timline' => 'Timeline'
22 static function init($ar)
25 foreach($ar as $k=>$v) {
31 static function registerEventTypes($objecttype, $events) {
32 self::$possible_event_types[$objecttype] = $events;
35 static function getWatchUI($object, $id) {
37 self::renderWatchUI($object, $id);
38 $res = ob_get_contents();
43 static function renderWatchUI($object, $id) {
44 $me = mtrack_canon_username(MTrackAuth::whoami());
45 if ($me == 'anonymous' || MTrackAuth::getUserClass() == 'anonymous') {
50 $url = $ABSWEB . 'admin/watch.php?' .
51 http_build_query(array('o' => $object, 'i' => $id));
52 $evts = json_encode(self::$possible_event_types[$object]);
53 $media = json_encode(self::$media);
55 foreach (MTrackDB::q('select medium, event from watches where otype = ? and oid = ? and userid = ? and active = 1', $object, $id, $me)->fetchAll() as $row)
57 $val->{$row['medium']}->{$row['event']} = true;
59 $val = json_encode($val);
61 <button id='watcher-$object-$id' type='button'>Watch</button>
63 $(document).ready(function () {
67 $('#watcher-$object-$id').click(function () {
68 var dlg = $('<div title="Watching"/>');
69 var frm = $('<form/>');
70 var tbl = $('<table/>');
72 tr.append('<th>Event</th>');
73 for (var m in media) {
86 for (var m in media) {
88 var cb = $('<input type="checkbox"/>');
89 if (V[m] && V[m][i]) {
90 cb.attr('checked', 'checked');
110 $("input[type='checkbox'][checked]", dlg).each(function () {
111 var m = $(this).data('medium');
112 var e = $(this).data('event');
118 $.post('$url', {w: JSON.stringify(V)});
119 $(this).dialog('close');
130 /* Returns an array, keyed by watching entity, of objects that changed
131 * since the specified date.
132 * $watcher = null means all watchers, otherwise specifies the only watcher of interest.
133 * $medium specifies timeline or email (or some other medium)
135 static function getWatchedItemsAndWatchers($since, $medium, $watcher = null) {
137 $q = MTrackDB::q('select otype, oid, userid, event from watches where active = 1 and medium = ? and userid = ?', $medium, $watcher);
139 $q = MTrackDB::q('select otype, oid, userid, event from watches where active = 1 and medium = ?', $medium);
141 $watches = $q->fetchAll(PDO::FETCH_ASSOC);
143 $last = strtotime($since);
146 $db = MTrackDB::get();
147 $changes = MTrackDB::q(
148 "select * from changes where changedate > ? order by changedate asc",
149 MTrackDB::unixtime($last))->fetchAll(PDO::FETCH_OBJ);
151 $cs_by_cid = array();
152 $by_object = array();
153 foreach ($changes as $CS) {
155 $cs_by_cid[$CS->cid] = $CS;
156 $t = strtotime($CS->changedate);
161 list($object, $id) = explode(':', $CS->object, 3);
162 $by_object[$object][$id][] = $CS->cid;
165 $repo_by_id = array();
166 $changesets_by_repo_and_rev = array();
167 $related_projects = array();
169 foreach (MTrackDB::q('select id from repos')
170 ->fetchAll(PDO::FETCH_COLUMN, 0) as $id) {
171 $repo = MTrack_Repo::loadById($id);
172 $repo_by_id[$id] = $repo;
174 foreach ($repo->history(null, MTrackDB::unixtime($last)) as $e) {
175 /* SCM doesn't always respect our date range */
176 $t = strtotime($e->ctime);
184 $key = $repo->getBrowseRootName() . ',' . $e->rev;
186 $changesets_by_repo_and_rev[$key] = $e;
188 $e->_related = array();
190 /* relationships to projects based on path */
191 $projid = $repo->projectFromPath($e->files);
192 if ($projid !== null) {
193 $e->_related[] = array('project', $projid);
194 $related_projects[$projid] = $projid;
199 /* Ensure that changesets are sorted chronologically */
200 uasort($changesets_by_repo_and_rev, array('MTrackWatch', '_compare_cs'));
202 /* Look at the changed tickets: match the reason back to one of the
203 * above changesets */
204 if (isset($by_object['ticket'])) {
205 foreach ($by_object['ticket'] as $tid => $cslist) {
206 foreach ($cslist as $cid) {
207 $CS = $cs_by_cid[$cid];
209 "/\(In \[changeset:(([^,]+),([a-zA-Z0-9]+))\]\)/sm",
210 $CS->reason, $CSM)) {
214 // $CSM[3] => changeset
215 foreach ($CSM[2] as $csm => $csm_repo) {
216 $csm_rev = $CSM[3][$csm];
218 /* Look for the repo changeset */
219 $key = "$csm_repo,$csm_rev";
220 if (isset($changesets_by_repo_and_rev[$key])) {
221 $e = $changesets_by_repo_and_rev[$key];
231 $proj_by_tid = array();
232 $emails_by_tid = array();
233 $emails_by_pid = array();
234 $owners_by_csid = array();
235 $milestones_by_tid = array();
236 $milestones_by_cid = array();
238 /* determine linked projects and group emails */
239 if (count($related_projects)) {
240 $projlist = join(',', $related_projects);
241 foreach (MTrackDB::q(
242 "select projid, notifyemail from projects where
243 notifyemail is not null and projid in ($projlist)")
244 ->fetchAll(PDO::FETCH_NUM) as $row) {
245 $emails_by_pid[$row[0]] = $row[1];
249 if (isset($by_object['ticket'])) {
250 $tkt_owner_ids = array();
251 $tkt_cid_list = array();
252 $tkt_milestone_fields = array();
254 foreach ($by_object['ticket'] as $tid => $cidlist) {
255 $tkt_list[] = $db->quote($tid);
256 $tkt_owner_ids[] = $db->quote("ticket:$tid:owner");
257 foreach ($cidlist as $cid) {
258 $tkt_cid_list[$cid] = $cid;
260 /* also want to include folks that were Cc'd */
261 $tkt_owner_ids[] = $db->quote("ticket:$tid:cc");
263 $tkt_milestone_fields[] = $db->quote("ticket:$tid:@milestones");
265 $tkt_list = join(',', $tkt_list);
267 foreach (MTrackDB::q(
268 "select t.tid, p.projid, notifyemail from tickets t left join ticket_components tc on t.tid = tc.tid left join components_by_project cbp on cbp.compid = tc.compid left join projects p on cbp.projid = p.projid where p.projid is not null and t.tid in ($tkt_list)")->fetchAll(PDO::FETCH_NUM) as $row) {
269 $proj_by_tid[$row[0]][$row[1]] = $row[1];
270 if (isset($row[2]) && strlen($row[2])) {
271 $emails_by_tid[$row[0]] = $row[2];
272 $emails_by_pid[$row[1]] = $row[2];
276 /* determine all changed owners in the affected period */
277 $tkt_owner_ids = join(',', $tkt_owner_ids);
278 $tkt_cid_list = join(',', $tkt_cid_list);
279 foreach (MTrackDB::q(
280 "select cid, oldvalue, value from change_audit where cid in ($tkt_cid_list) and fieldname in ($tkt_owner_ids)")->fetchAll(PDO::FETCH_NUM) as $row) {
281 $cid = array_shift($row);
282 foreach ($row as $owner) {
283 if (!strlen($owner)) continue;
284 $owners_by_csid[$cid][$owner] = mtrack_canon_username($owner);
288 /* determine all changed milestones in the affected period */
289 $tkt_milestone_fields = join(',', $tkt_milestone_fields);
290 foreach (MTrackDB::q(
291 "select cid, oldvalue, value from change_audit where cid in ($tkt_cid_list) and fieldname in ($tkt_milestone_fields)")->fetchAll(PDO::FETCH_NUM) as $row) {
292 $cid = array_shift($row);
293 foreach ($row as $ms) {
294 $ms = split(',', $ms);
295 foreach ($ms as $mid) {
297 $milestones_by_cid[$cid][$mid] = $mid;
302 foreach (MTrackDB::q(
303 "select tid, mid from ticket_milestones where tid in ($tkt_list)")
304 ->fetchAll(PDO::FETCH_NUM) as $row) {
305 $milestones_by_tid[$row[0]][$row[1]] = $row[1];
309 /* walk through list of objects and add related objects */
310 if (isset($by_object['ticket'])) {
311 foreach ($by_object['ticket'] as $tid => $cslist) {
312 foreach ($cslist as $cid) {
313 $CS = $cs_by_cid[$cid];
314 if (!isset($CS->_related)) {
315 $CS->_related = array();
318 if (isset($CS->ent)) {
319 $CS->_related[] = array('repo', $CS->ent->repo->id);
321 if (isset($proj_by_tid[$tid])) {
322 foreach ($proj_by_tid[$tid] as $pid) {
323 $CS->_related[] = array('project', $pid);
326 if (isset($milestones_by_tid[$tid])) {
327 foreach ($milestones_by_tid[$tid] as $mid) {
328 $CS->_related[] = array('milestone', $mid);
331 if (isset($milestones_by_cid[$cid])) {
332 foreach ($milestones_by_cid[$cid] as $mid) {
333 $CS->_related[] = array('milestone', $mid);
339 foreach ($changesets_by_repo_and_rev as $ent) {
340 $ent->_related[] = array('repo', $ent->repo->id);
343 /* having determined all changed items, make a pass through to determine
344 * how to associate those with watchers.
345 * Watchers are one of:
346 * - an user with a matching watches entry
347 * - the group email address associated with a project associated with the
349 * - the owner of a ticket
352 /* generate synthetic watcher entries for project group emails */
353 foreach ($emails_by_pid as $pid => $email) {
355 'otype' => 'project',
362 foreach ($by_object as $otype => $objects) {
363 foreach ($objects as $oid => $cidlist) {
364 foreach ($cidlist as $cid) {
365 $CS = $cs_by_cid[$cid];
366 if (isset($owners_by_csid[$cid])) {
367 /* add synthetic watcher for a past or current owner */
368 foreach ($owners_by_csid[$cid] as $owner) {
377 self::_compute_watch($watches, $otype, $oid, $CS);
378 /* eliminate from the set if there are no watchers */
379 if (!isset($CS->_watcher)) {
380 unset($cs_by_cid[$cid]);
385 foreach ($changesets_by_repo_and_rev as $key => $ent) {
386 self::_compute_watch($watches, 'changeset', $key, $ent);
387 /* eliminate from the set if there are no watchers */
388 if (!isset($ent->_watcher)) {
389 unset($changesets_by_repo_and_rev[$key]);
393 /* now collect the data by watcher */
394 $by_watcher = array();
395 foreach ($cs_by_cid as $CS) {
396 foreach ($CS->_watcher as $user) {
397 $by_watcher[$user][$CS->cid] = $CS;
400 foreach ($changesets_by_repo_and_rev as $key => $ent) {
401 foreach ($ent->_watcher as $user) {
402 /* don't add this if we have an associated CS */
403 if (isset($ent->CS) && $by_watcher[$user][$ent->CS->cid]) {
406 $by_watcher[$user][$key] = $ent;
409 /* one last pass to group the data by object */
410 foreach ($by_watcher as $user => $items) {
411 foreach ($items as $key => $obj) {
412 if ($obj instanceof MTrackSCMEvent) {
414 $nkey = "repo:" . $obj->repo->id;
416 $nkey = $obj->object;
418 unset($by_watcher[$user][$key]);
419 $by_watcher[$user][$nkey][] = $obj;
426 static function _compute_watch($watches, $otype, $oid, $obj, $event = null) {
427 foreach ($watches as $row) {
428 if ($row['otype'] != $otype) continue;
429 if ($row['oid'] != '*' && $row['oid'] != $oid) continue;
430 if ($event === null || $row['event'] == '*' || $row['event'] == $event) {
431 if (!isset($obj->_watcher)) {
432 $obj->_watcher = array();
434 $obj->_watcher[$row['userid']] = $row['userid'];
437 if ($event === null && isset($obj->_related)) {
438 foreach ($obj->_related as $rel) {
439 self::_compute_watch($watches, $rel[0], $rel[1], $obj, $otype);
444 static function _get_project($pid) {
445 static $projects = array();
446 if (isset($projects[$pid])) {
447 return $projects[$pid];
449 $projects[$pid] = MTrackProject::loadById($pid);
450 return $projects[$pid];
453 /* comparison function for MTrackSCMEvent objects that sorts in ascending
454 * chronological order */
455 static function _compare_cs($A, $B)
457 return strcmp($A->ctime, $B->ctime);
459 // mostly used to force sign up by assigned person..
460 // MTrackWatch::watch_object('ticket', xxxx, user);
461 static function watch_object($objname, $id, $user)
464 $db = MTrackDB::get();
465 MTrackDB::q('delete from watches where otype = ? and oid = ? and userid = ?',
466 $objname, $id, $user);
468 $evts = self::$possible_event_types[$objname];
470 foreach ($evts as $evt) {
471 MTrackDB::q('insert into watches (otype, oid, userid, medium, event, active) values (?, ?, ?, ?, ?, 1)',
472 $objname, $id, $user, 'email', $evt);
478 static function objectWatchersNameId($objname, $objid)
480 require_once 'MTrack/DataObjects/Userinfo.php';
482 $q = MTrackDB::q('select distinct(userid) from watches where otype = ? and oid = ?',
486 foreach ($q->fetchAll(PDO::FETCH_ASSOC) as $CS) {
488 $ret[] = MTrack_DataObjects_Userinfo::get($CS['userid']);
495 static function objectWatchers($object)
497 return self::objectWatchersNameId( $object->watchType(), $object->watchId());