Switch to side-by-side view

--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -522,6 +522,53 @@
                 attachment.filename, attachment.file,
                 content_type=attachment.type)
 
+    def move(self, app_config):
+        '''Move ticket from current tickets app to tickets app with given app_config'''
+        self.globals.invalidate_bin_counts()
+        app = app_config.project.app_instance(app_config)
+        prior_url = self.url()
+        prior_app = self.app
+
+        # ensure unique ticket_num
+        while True:
+            with h.push_context(app_config.project_id, app_config_id=app_config._id):
+                ticket_num = app.globals.next_ticket_num()
+            self.ticket_num = ticket_num
+            self.app_config_id = app_config._id
+            try:
+                session(self).flush(self)
+                h.log_action(log, 'moved').info('Ticket %s moved to %s' % (prior_url, self.url()))
+                break
+            except OperationFailure, err:
+                if 'duplicate' in err.args[0]:
+                    log.warning('Try to create duplicate ticket %s when moving from %s' % (self.url(), prior_url))
+                    session(self).expunge(self)
+                    continue
+
+        prior_cfs = [
+            (cf['name'], cf['type'], cf['label'])
+            for cf in prior_app.globals.custom_fields or []]
+        new_cfs = [
+            (cf['name'], cf['type'], cf['label'])
+            for cf in app.globals.custom_fields or []]
+        skipped_fields = []
+        for cf in prior_cfs:
+            if cf not in new_cfs:
+                skipped_fields.append(cf)
+
+        message = 'Ticket moved from %s' % prior_url
+        if skipped_fields:
+            message += '\n\nCan\'t be converted:\n'
+            for cf in skipped_fields:
+                name = cf[0]
+                message += '\n- **%s**: %s' % (name, self.custom_fields[name])
+        self.discussion_thread.post(text=message)
+
+        # need this to reset app_config RelationProperty on ticket to a new one
+        session(self).expunge(self)
+        return Ticket.query.find(dict(
+            app_config_id=app_config._id, ticket_num=self.ticket_num)).first()
+
     def __json__(self):
         return dict(super(Ticket,self).__json__(),
             created_date=self.created_date,