Switch to side-by-side view

--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -1,5 +1,5 @@
 from time import sleep
-from datetime import datetime
+from datetime import datetime, timedelta
 
 import urllib
 import tg
@@ -13,6 +13,7 @@
 from pyforge.model import Artifact, VersionedArtifact, Snapshot, Message, project_orm_session, Project
 from pyforge.model import File, User, Feed, Thread, Post, Notification
 from pyforge.lib import helpers as h
+from pyforge.lib.search import search_artifact
 
 common_suffix = tg.config.get('forgemail.domain', '.sourceforge.net')
 
@@ -29,6 +30,8 @@
     status_names = FieldProperty(str)
     milestone_names = FieldProperty(str, if_missing='')
     custom_fields = FieldProperty([{str:None}])
+    _bin_counts = FieldProperty({str:int})
+    _bin_counts_expire = FieldProperty(datetime)
 
     @classmethod
     def next_ticket_num(cls):
@@ -37,6 +40,22 @@
             update={'$inc': { 'last_ticket_num': 1}},
             new=True)
         return g.last_ticket_num+1
+
+    @property
+    def bin_counts(self):
+        if self._bin_counts_expire is None or datetime.utcnow() > self._bin_counts_expire:
+            for b in Bin.query.find(dict(
+                    app_config_id=self.app_config_id)):
+                r = search_artifact(Ticket, b.terms, rows=0)
+                self._bin_counts[b.summary] = r.hits
+            self._bin_counts_expire = datetime.utcnow() + timedelta(minutes=60)
+        return self._bin_counts
+
+    def invalidate_bin_counts(self):
+        '''Expire it just a bit in the future to allow data to propagate through
+        the search reactors
+        '''
+        self._bin_counts_expire = datetime.utcnow() + timedelta(seconds=5)
 
     @classmethod
     def for_current_tracker(cls):