1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
5 static $possible_event_types = array();
8 // 'timline' => 'Timeline'
11 static function registerEventTypes($objecttype, $events) {
12 self::$possible_event_types[$objecttype] = $events;
15 static function getWatchUI($object, $id) {
17 self::renderWatchUI($object, $id);
18 $res = ob_get_contents();
23 static function renderWatchUI($object, $id) {
24 $me = mtrack_canon_username(MTrackAuth::whoami());
25 if ($me == 'anonymous' || MTrackAuth::getUserClass() == 'anonymous') {
30 $url = $ABSWEB . 'admin/watch.php?' .
31 http_build_query(array('o' => $object, 'i' => $id));
32 $evts = json_encode(self::$possible_event_types[$object]);
33 $media = json_encode(self::$media);
35 foreach (MTrackDB::q('select medium, event from watches where otype = ? and oid = ? and userid = ? and active = 1', $object, $id, $me)->fetchAll() as $row)
37 $val->{$row['medium']}->{$row['event']} = true;
39 $val = json_encode($val);
41 <button id='watcher-$object-$id' type='button'>Watch</button>
43 $(document).ready(function () {
47 $('#watcher-$object-$id').click(function () {
48 var dlg = $('<div title="Watching"/>');
49 var frm = $('<form/>');
50 var tbl = $('<table/>');
52 tr.append('<th>Event</th>');
53 for (var m in media) {
66 for (var m in media) {
68 var cb = $('<input type="checkbox"/>');
69 if (V[m] && V[m][i]) {
70 cb.attr('checked', 'checked');
90 $("input[type='checkbox'][checked]", dlg).each(function () {
91 var m = $(this).data('medium');
92 var e = $(this).data('event');
98 $.post('$url', {w: JSON.stringify(V)});
99 $(this).dialog('close');
110 /* Returns an array, keyed by watching entity, of objects that changed
111 * since the specified date.
112 * $watcher = null means all watchers, otherwise specifies the only watcher of interest.
113 * $medium specifies timeline or email (or some other medium)
115 static function getWatchedItemsAndWatchers($since, $medium, $watcher = null) {
117 $q = MTrackDB::q('select otype, oid, userid, event from watches where active = 1 and medium = ? and userid = ?', $medium, $watcher);
119 $q = MTrackDB::q('select otype, oid, userid, event from watches where active = 1 and medium = ?', $medium);
121 $watches = $q->fetchAll(PDO::FETCH_ASSOC);
123 $last = strtotime($since);
126 $db = MTrackDB::get();
127 $changes = MTrackDB::q(
128 "select * from changes where changedate > ? order by changedate asc",
129 MTrackDB::unixtime($last))->fetchAll(PDO::FETCH_OBJ);
131 $cs_by_cid = array();
132 $by_object = array();
133 foreach ($changes as $CS) {
135 $cs_by_cid[$CS->cid] = $CS;
136 $t = strtotime($CS->changedate);
141 list($object, $id) = explode(':', $CS->object, 3);
142 $by_object[$object][$id][] = $CS->cid;
145 $repo_by_id = array();
146 $changesets_by_repo_and_rev = array();
147 $related_projects = array();
149 foreach (MTrackDB::q('select repoid from repos')
150 ->fetchAll(PDO::FETCH_COLUMN, 0) as $repoid) {
151 $repo = MTrackRepo::loadById($repoid);
152 $repo_by_id[$repoid] = $repo;
154 foreach ($repo->history(null, MTrackDB::unixtime($last)) as $e) {
155 /* SCM doesn't always respect our date range */
156 $t = strtotime($e->ctime);
164 $key = $repo->getBrowseRootName() . ',' . $e->rev;
166 $changesets_by_repo_and_rev[$key] = $e;
168 $e->_related = array();
170 /* relationships to projects based on path */
171 $projid = $repo->projectFromPath($e->files);
172 if ($projid !== null) {
173 $e->_related[] = array('project', $projid);
174 $related_projects[$projid] = $projid;
179 /* Ensure that changesets are sorted chronologically */
180 uasort($changesets_by_repo_and_rev, array('MTrackWatch', '_compare_cs'));
182 /* Look at the changed tickets: match the reason back to one of the
183 * above changesets */
184 if (isset($by_object['ticket'])) {
185 foreach ($by_object['ticket'] as $tid => $cslist) {
186 foreach ($cslist as $cid) {
187 $CS = $cs_by_cid[$cid];
189 "/\(In \[changeset:(([^,]+),([a-zA-Z0-9]+))\]\)/sm",
190 $CS->reason, $CSM)) {
194 // $CSM[3] => changeset
195 foreach ($CSM[2] as $csm => $csm_repo) {
196 $csm_rev = $CSM[3][$csm];
198 /* Look for the repo changeset */
199 $key = "$csm_repo,$csm_rev";
200 if (isset($changesets_by_repo_and_rev[$key])) {
201 $e = $changesets_by_repo_and_rev[$key];
211 $proj_by_tid = array();
212 $emails_by_tid = array();
213 $emails_by_pid = array();
214 $owners_by_csid = array();
215 $milestones_by_tid = array();
216 $milestones_by_cid = array();
218 /* determine linked projects and group emails */
219 if (count($related_projects)) {
220 $projlist = join(',', $related_projects);
221 foreach (MTrackDB::q(
222 "select projid, notifyemail from projects where
223 notifyemail is not null and projid in ($projlist)")
224 ->fetchAll(PDO::FETCH_NUM) as $row) {
225 $emails_by_pid[$row[0]] = $row[1];
229 if (isset($by_object['ticket'])) {
230 $tkt_owner_ids = array();
231 $tkt_cid_list = array();
232 $tkt_milestone_fields = array();
234 foreach ($by_object['ticket'] as $tid => $cidlist) {
235 $tkt_list[] = $db->quote($tid);
236 $tkt_owner_ids[] = $db->quote("ticket:$tid:owner");
237 foreach ($cidlist as $cid) {
238 $tkt_cid_list[$cid] = $cid;
240 /* also want to include folks that were Cc'd */
241 $tkt_owner_ids[] = $db->quote("ticket:$tid:cc");
243 $tkt_milestone_fields[] = $db->quote("ticket:$tid:@milestones");
245 $tkt_list = join(',', $tkt_list);
247 foreach (MTrackDB::q(
248 "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) {
249 $proj_by_tid[$row[0]][$row[1]] = $row[1];
250 if (isset($row[2]) && strlen($row[2])) {
251 $emails_by_tid[$row[0]] = $row[2];
252 $emails_by_pid[$row[1]] = $row[2];
256 /* determine all changed owners in the affected period */
257 $tkt_owner_ids = join(',', $tkt_owner_ids);
258 $tkt_cid_list = join(',', $tkt_cid_list);
259 foreach (MTrackDB::q(
260 "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) {
261 $cid = array_shift($row);
262 foreach ($row as $owner) {
263 if (!strlen($owner)) continue;
264 $owners_by_csid[$cid][$owner] = mtrack_canon_username($owner);
268 /* determine all changed milestones in the affected period */
269 $tkt_milestone_fields = join(',', $tkt_milestone_fields);
270 foreach (MTrackDB::q(
271 "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) {
272 $cid = array_shift($row);
273 foreach ($row as $ms) {
274 $ms = split(',', $ms);
275 foreach ($ms as $mid) {
277 $milestones_by_cid[$cid][$mid] = $mid;
282 foreach (MTrackDB::q(
283 "select tid, mid from ticket_milestones where tid in ($tkt_list)")
284 ->fetchAll(PDO::FETCH_NUM) as $row) {
285 $milestones_by_tid[$row[0]][$row[1]] = $row[1];
289 /* walk through list of objects and add related objects */
290 if (isset($by_object['ticket'])) {
291 foreach ($by_object['ticket'] as $tid => $cslist) {
292 foreach ($cslist as $cid) {
293 $CS = $cs_by_cid[$cid];
294 if (!isset($CS->_related)) {
295 $CS->_related = array();
298 if (isset($CS->ent)) {
299 $CS->_related[] = array('repo', $CS->ent->repo->repoid);
301 if (isset($proj_by_tid[$tid])) {
302 foreach ($proj_by_tid[$tid] as $pid) {
303 $CS->_related[] = array('project', $pid);
306 if (isset($milestones_by_tid[$tid])) {
307 foreach ($milestones_by_tid[$tid] as $mid) {
308 $CS->_related[] = array('milestone', $mid);
311 if (isset($milestones_by_cid[$cid])) {
312 foreach ($milestones_by_cid[$cid] as $mid) {
313 $CS->_related[] = array('milestone', $mid);
319 foreach ($changesets_by_repo_and_rev as $ent) {
320 $ent->_related[] = array('repo', $ent->repo->repoid);
323 /* having determined all changed items, make a pass through to determine
324 * how to associate those with watchers.
325 * Watchers are one of:
326 * - an user with a matching watches entry
327 * - the group email address associated with a project associated with the
329 * - the owner of a ticket
332 /* generate synthetic watcher entries for project group emails */
333 foreach ($emails_by_pid as $pid => $email) {
335 'otype' => 'project',
342 foreach ($by_object as $otype => $objects) {
343 foreach ($objects as $oid => $cidlist) {
344 foreach ($cidlist as $cid) {
345 $CS = $cs_by_cid[$cid];
346 if (isset($owners_by_csid[$cid])) {
347 /* add synthetic watcher for a past or current owner */
348 foreach ($owners_by_csid[$cid] as $owner) {
357 self::_compute_watch($watches, $otype, $oid, $CS);
358 /* eliminate from the set if there are no watchers */
359 if (!isset($CS->_watcher)) {
360 unset($cs_by_cid[$cid]);
365 foreach ($changesets_by_repo_and_rev as $key => $ent) {
366 self::_compute_watch($watches, 'changeset', $key, $ent);
367 /* eliminate from the set if there are no watchers */
368 if (!isset($ent->_watcher)) {
369 unset($changesets_by_repo_and_rev[$key]);
373 /* now collect the data by watcher */
374 $by_watcher = array();
375 foreach ($cs_by_cid as $CS) {
376 foreach ($CS->_watcher as $user) {
377 $by_watcher[$user][$CS->cid] = $CS;
380 foreach ($changesets_by_repo_and_rev as $key => $ent) {
381 foreach ($ent->_watcher as $user) {
382 /* don't add this if we have an associated CS */
383 if (isset($ent->CS) && $by_watcher[$user][$ent->CS->cid]) {
386 $by_watcher[$user][$key] = $ent;
389 /* one last pass to group the data by object */
390 foreach ($by_watcher as $user => $items) {
391 foreach ($items as $key => $obj) {
392 if ($obj instanceof MTrackSCMEvent) {
394 $nkey = "repo:" . $obj->repo->repoid;
396 $nkey = $obj->object;
398 unset($by_watcher[$user][$key]);
399 $by_watcher[$user][$nkey][] = $obj;
406 static function _compute_watch($watches, $otype, $oid, $obj, $event = null) {
407 foreach ($watches as $row) {
408 if ($row['otype'] != $otype) continue;
409 if ($row['oid'] != '*' && $row['oid'] != $oid) continue;
410 if ($event === null || $row['event'] == '*' || $row['event'] == $event) {
411 if (!isset($obj->_watcher)) {
412 $obj->_watcher = array();
414 $obj->_watcher[$row['userid']] = $row['userid'];
417 if ($event === null && isset($obj->_related)) {
418 foreach ($obj->_related as $rel) {
419 self::_compute_watch($watches, $rel[0], $rel[1], $obj, $otype);
424 static function _get_project($pid) {
425 static $projects = array();
426 if (isset($projects[$pid])) {
427 return $projects[$pid];
429 $projects[$pid] = MTrackProject::loadById($pid);
430 return $projects[$pid];
433 /* comparison function for MTrackSCMEvent objects that sorts in ascending
434 * chronological order */
435 static function _compare_cs($A, $B) {
436 return strcmp($A->ctime, $B->ctime);