Switch to side-by-side view

--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -86,7 +86,8 @@
     _id = FieldProperty(schema.ObjectId)
     created_date = FieldProperty(datetime, if_missing=datetime.utcnow)
 
-    parent_id = FieldProperty(schema.ObjectId, if_missing=None)
+    super_id = FieldProperty(schema.ObjectId, if_missing=None)
+    sub_ids = FieldProperty([schema.ObjectId], if_missing=None)
     ticket_num = FieldProperty(int)
     summary = FieldProperty(str)
     description = FieldProperty(str, if_missing='')
@@ -191,6 +192,72 @@
         c = Comment(ticket_id=self._id, text=text)
         return c
 
+    def set_as_subticket_of(self, new_super_id):
+        # For this to be generally useful we would have to check first that
+        # new_super_id is not a sub_id (recursively) of self
+
+        if self.super_id == new_super_id:
+            return
+
+        if self.super_id is not None:
+            old_super = Ticket.query.get(_id=self.super_id, app_config_id=c.app.config._id)
+            old_super.sub_ids = [id for id in old_super.sub_ids if id != self._id]
+            old_super.dirty_sums(dirty_self=True)
+
+        self.super_id = new_super_id
+
+        if new_super_id is not None:
+            new_super = Ticket.query.get(_id=new_super_id, app_config_id=c.app.config._id)
+            if new_super.sub_ids is None:
+                new_super.sub_ids = []
+            if self._id not in new_super.sub_ids:
+                new_super.sub_ids.append(self._id)
+            new_super.dirty_sums(dirty_self=True)
+
+    def recalculate_sums(self, super_sums=None):
+        """Calculate custom fields of type 'sum' (if any) by recursing into subtickets (if any)."""
+        if super_sums is None:
+            super_sums = {}
+            globals = Globals.query.get(app_config_id=c.app.config._id)
+            for k in [cf.name for cf in globals.custom_fields or [] if cf.type=='sum']:
+                super_sums[k] = float(0)
+
+        # if there are no custom fields of type 'sum', we're done
+        if not super_sums:
+            return
+
+        # if this ticket has no subtickets, use its field values directly
+        if not self.sub_ids:
+            for k in super_sums:
+                try:
+                    v = float(self.custom_fields.get(k, 0))
+                except ValueError:
+                    v = 0
+                super_sums[k] += v
+
+        # else recurse into subtickets
+        else:
+            sub_sums = {}
+            for k in super_sums:
+                sub_sums[k] = float(0)
+            for id in self.sub_ids:
+                subticket = Ticket.query.get(_id=id, app_config_id=c.app.config._id)
+                subticket.recalculate_sums(sub_sums)
+            for k, v in sub_sums.iteritems():
+                self.custom_fields[k] = v
+                super_sums[k] += v
+
+    def dirty_sums(self, dirty_self=False):
+        """From a changed ticket, climb the superticket chain to call recalculate_sums at the root."""
+        root = self if dirty_self else None
+        next_id = self.super_id
+        while next_id is not None:
+            root = Ticket.query.get(_id=next_id, app_config_id=c.app.config._id)
+            next_id = root.super_id
+        if root is not None:
+            root.recalculate_sums()
+
+
 class Comment(Message):
 
     class __mongometa__: