Switch to side-by-side view

--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -54,8 +54,7 @@
     _bin_counts = FieldProperty(schema.Deprecated) # {str:int})
     _bin_counts_data = FieldProperty([dict(summary=str, hits=int)])
     _bin_counts_expire = FieldProperty(datetime)
-    _milestone_counts = FieldProperty([dict(name=str,hits=int,closed=int)])
-    _milestone_counts_expire = FieldProperty(datetime)
+    _bin_counts_invalidated = FieldProperty(datetime)
     show_in_search = FieldProperty({str: bool}, if_missing={'ticket_num': True,
                                                             'summary': True,
                                                             '_milestone': True,
@@ -115,7 +114,7 @@
                 return fld
         return None
 
-    def _refresh_counts(self):
+    def update_bin_counts(self):
         # Refresh bin counts
         self._bin_counts_data = []
         for b in Bin.query.find(dict(
@@ -125,10 +124,13 @@
             self._bin_counts_data.append(dict(summary=b.summary, hits=hits))
         self._bin_counts_expire = \
             datetime.utcnow() + timedelta(minutes=60)
+        self._bin_counts_invalidated = None
 
     def bin_count(self, name):
+        # not sure why we expire bin counts after an hour even if unchanged
+        # I guess a catch-all in case invalidate_bin_counts is missed
         if self._bin_counts_expire < datetime.utcnow():
-            self._refresh_counts()
+            self.invalidate_bin_counts()
         for d in self._bin_counts_data:
             if d['summary'] == name: return d
         return dict(summary=name, hits=0)
@@ -148,10 +150,19 @@
         return d
 
     def invalidate_bin_counts(self):
-        '''Expire it just a bit in the future to allow data to propagate through
-        the search task
-        '''
-        self._bin_counts_expire = datetime.utcnow() + timedelta(seconds=5)
+        '''Force expiry of bin counts and queue them to be updated.'''
+        # To prevent multiple calls to this method from piling on redundant
+        # tasks, we set _bin_counts_invalidated when we post the task, and
+        # the task clears it when it's done.  However, in the off chance
+        # that the task fails or is interrupted, we ignore the flag if it's
+        # older than 5 minutes.
+        invalidation_expiry = datetime.utcnow() - timedelta(minutes=5)
+        if self._bin_counts_invalidated is not None and \
+           self._bin_counts_invalidated > invalidation_expiry:
+            return
+        self._bin_counts_invalidated = datetime.utcnow()
+        from forgetracker import tasks  # prevent circular import
+        tasks.update_bin_counts.post(self.app_config_id)
 
     def sortable_custom_fields_shown_in_search(self):
         return [dict(sortable_name='%s_s' % field['name'],
@@ -481,7 +492,6 @@
             app_config_id=self.app_config_id, artifact_id=self._id, type='attachment'))
 
     def update(self, ticket_form):
-        self.globals.invalidate_bin_counts()
         # update is not allowed to change the ticket_num
         ticket_form.pop('ticket_num', None)
         self.labels = ticket_form.pop('labels', [])
@@ -583,7 +593,6 @@
             custom_fields[fn] = old_val
         self.custom_fields = custom_fields
 
-        self.globals.invalidate_bin_counts()
         # move ticket. ensure unique ticket_num
         while True:
             with h.push_context(app_config.project_id, app_config_id=app_config._id):