diff --git a/include/class.attachment.php b/include/class.attachment.php
index 2a2fa7dc595cbfd274342f579294387c0acf5107..748266003819edd0fbde1f248b0afdf15657a8a8 100644
--- a/include/class.attachment.php
+++ b/include/class.attachment.php
@@ -78,9 +78,9 @@ class Attachment extends VerySimpleModel {
     }
 
     static function lookup($var, $objectId=0) {
-        return is_numeric($var)
-            ? parent::lookup($var)
-            : static::lookupByFileHash($var, $objectId);
+        return (is_string($var))
+            ? static::lookupByFileHash($var, $objectId)
+            : parent::lookup($var);
     }
 }
 
diff --git a/include/class.client.php b/include/class.client.php
index a8b9da332337c5e34a89fb1db09b6a925f2b278f..4932f4c400adfd6ab4d528fe3bb69853171e0aff 100644
--- a/include/class.client.php
+++ b/include/class.client.php
@@ -81,10 +81,9 @@ implements EmailContact, ITicketUser {
                     $user = null;
                 break;
             case 'o': //Ticket owner
-                if (($ticket = Ticket::lookup($matches['tid']))) {
-                    if (($user = $ticket->getOwner())
-                            && $user->getId() != $matches['uid'])
-                        $user = null;
+                if (($user = $ticket->getOwner())
+                        && $user->getId() != $matches['uid']) {
+                    $user = null;
                 }
                 break;
         }
diff --git a/include/class.collaborator.php b/include/class.collaborator.php
index e7c65cec671f054613073e85aabb43d24b040ecb..35e5ea0287185080469a3f947bf046359c1be48d 100644
--- a/include/class.collaborator.php
+++ b/include/class.collaborator.php
@@ -23,6 +23,7 @@ implements EmailContact, ITicketUser {
     static $meta = array(
         'table' => THREAD_COLLABORATOR_TABLE,
         'pk' => array('id'),
+        'select_related' => array('user'),
         'joins' => array(
             'thread' => array(
                 'constraint' => array('thread_id' => 'Thread.id'),
@@ -51,14 +52,14 @@ implements EmailContact, ITicketUser {
     }
 
     function getTicketId() {
-        if ($this->thread->ticket)
+        if ($this->thread->object_type == ObjectModel::OBJECT_TYPE_TICKET)
             return $this->thread->object_id;
     }
 
     function getTicket() {
         // TODO: Change to $this->thread->ticket when Ticket goes to ORM
-        $ticket = Ticket::lookup($this->getTicketId());
-        return $ticket;
+        if ($id = $this->getTicketId())
+            return Ticket::lookup($id);
     }
 
     function getUser() {
@@ -72,6 +73,9 @@ implements EmailContact, ITicketUser {
     function getName() {
         return $this->user->getName();
     }
+    function sendAccessLink($ticket) {
+        return $this->user->sendAccessLink($ticket);
+    }
 
     // VariableReplacer interface
     function getVar($what) {
@@ -139,10 +143,10 @@ implements EmailContact, ITicketUser {
         return false;
     }
 
-    static function forTicket($tid, $criteria=array()) {
+    static function forThread($tid, $criteria=array()) {
 
         $collaborators = static::objects()
-            ->filter(array('thread__ticket__ticket_id' => $tid));
+            ->filter(array('thread_id' => $tid));
 
         if (isset($criteria['isactive']))
             $collaborators->filter(array('isactive' => $criteria['isactive']));
diff --git a/include/class.cron.php b/include/class.cron.php
index 7598d7ffe63af0b0bb28f6d1850d3cac2efc085c..94e1af314a5e97bb957c212ef40ec3765004a360 100644
--- a/include/class.cron.php
+++ b/include/class.cron.php
@@ -28,6 +28,10 @@ class Cron {
     function TicketMonitor() {
         require_once(INCLUDE_DIR.'class.ticket.php');
         Ticket::checkOverdue(); //Make stale tickets overdue
+        // Cleanup any expired locks
+        require_once(INCLUDE_DIR.'class.lock.php');
+        Lock::cleanup();
+
     }
 
     function PurgeLogs() {
diff --git a/include/class.lock.php b/include/class.lock.php
index be408cca36529e6e8a5406f13c281203415d0058..e4870d4652801659d68e6c77bc6d231991f378ea 100644
--- a/include/class.lock.php
+++ b/include/class.lock.php
@@ -113,9 +113,6 @@ class Lock extends VerySimpleModel {
         if (!$staffId or !$lockTime)
             return null;
 
-        // Cleanup any expired locks
-        self::cleanup();
-
         // Create the new lock.
         $lock = parent::create(array(
             'created' => SqlFunction::NOW(),
diff --git a/include/class.orm.php b/include/class.orm.php
index 92e28df8e8dc744bb2abb4789453c9e2ea470cf6..0e0568e44a05835f8db2638872f1d1d9dd25f837 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -92,10 +92,13 @@ class ModelMeta implements ArrayAccess {
                 $j['null'] = true;
         }
         // XXX: Make this better (ie. composite keys)
-        $keys = array_keys($j['constraint']);
-        $foreign = $j['constraint'][$keys[0]];
-        $j['fkey'] = explode('.', $foreign);
-        $j['local'] = $keys[0];
+        foreach ($j['constraint'] as $local => $foreign) {
+            list($class, $field) = explode('.', $foreign);
+            if ($local[0] == "'" || $field[0] == "'" || !class_exists($class))
+                continue;
+            $j['fkey'] = array($class, $field);
+            $j['local'] = $local;
+        }
     }
 
     function offsetGet($field) {
@@ -1971,7 +1974,8 @@ class MySqlCompiler extends SqlCompiler {
                     $fields[] = $T;
                 }
             }
-            if (!$queryset->values) {
+            // If no group by has been set yet, use the root model pk
+            if (!$group_by) {
                 foreach ($model::$meta['pk'] as $pk)
                     $group_by[] = $rootAlias .'.'. $pk;
             }
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 8e96ca3eb4b095e7479c52fb33323f13a1e2e857..b7560b8a15f8da4d8b2e475595cc76e4fc3ab6e5 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -292,7 +292,7 @@ implements RestrictedAccess, Threadable {
 
     function loadDynamicData() {
         if (!$this->_answers) {
-            foreach (DynamicFormEntry::forTicket($this->getId(), true) as $form) {
+            foreach (DynamicFormEntry::forTicket($this->getThreadId(), true) as $form) {
                 foreach ($form->getAnswers() as $answer) {
                     $tag = mb_strtolower($answer->getField()->get('name'))
                         ?: 'field.' . $answer->getField()->get('id');
@@ -650,12 +650,15 @@ implements RestrictedAccess, Threadable {
         return $this->tlock;
     }
 
-    function releaseLock() {
+    function releaseLock($staffId=false) {
         if (!($lock = $this->getLock()))
-            return;
+            return false;
+
+        if ($staffId && $lock->staff_id != $staffId)
+            return false;
 
         if (!$lock->delete())
-            return;
+            return false;
 
         $sql = 'UPDATE '.TICKET_TABLE.' SET `lock_id` = 0 WHERE `ticket_id` = '
             . db_input($this->getId());
@@ -900,10 +903,10 @@ implements RestrictedAccess, Threadable {
     function getCollaborators($criteria=array()) {
 
         if ($criteria)
-            return Collaborator::forTicket($this->getId(), $criteria);
+            return Collaborator::forThread($this->getThreadId(), $criteria);
 
         if (!isset($this->collaborators))
-            $this->collaborators = Collaborator::forTicket($this->getId());
+            $this->collaborators = Collaborator::forThread($this->getThreadId());
 
         return $this->collaborators;
     }
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index 50301d498d0aa43fede1319bca10dd4338678f0a..0560eaf00ce70eacee1048f45dd9903a13368843 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -514,7 +514,6 @@ $tcount = $ticket->getThreadEntries($types)->count();
         <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
         <input type="hidden" name="msgId" value="<?php echo $msgId; ?>">
         <input type="hidden" name="a" value="reply">
-        <input type="hidden" name="lockId" value="<?php echo $ticket->getLock()->getId(); ?>">
         <input type="hidden" name="lockCode" value="<?php echo $ticket->getLock()->getCode(); ?>">
         <span class="error"></span>
         <table style="width:100%" border="0" cellspacing="0" cellpadding="3">
@@ -704,7 +703,6 @@ $tcount = $ticket->getThreadEntries($types)->count();
         <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
         <input type="hidden" name="locktime" value="<?php echo $cfg->getLockTime(); ?>">
         <input type="hidden" name="a" value="postnote">
-        <input type="hidden" name="lockId" value="<?php echo $ticket->getLock()->getId(); ?>">
         <input type="hidden" name="lockCode" value="<?php echo $ticket->getLock()->getCode(); ?>">
         <table width="100%" border="0" cellspacing="0" cellpadding="3">
             <?php
@@ -1077,7 +1075,8 @@ $(function() {
 // Hover support for all inline images
 $urls = array();
 foreach (AttachmentFile::objects()->filter(array(
-    'attachments__thread_entry__thread__id' => $ticket->getThreadId()
+    'attachments__thread_entry__thread__id' => $ticket->getThreadId(),
+    'attachments__inline' => true,
 )) as $file) {
     $urls[strtolower($file->getKey())] = array(
         'download_url' => $file->getDownloadUrl(),
diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js
index 559b9a476d210e7d71dc752ac9cd66ee23bc3d7a..39ffaad3051debd0098e3c67400ebe5a8e405222 100644
--- a/js/redactor-osticket.js
+++ b/js/redactor-osticket.js
@@ -67,7 +67,7 @@ RedactorPlugins.draft = function() {
         // Slight workaround. Signal the 'keyup' event normally signaled
         // from typing in the <textarea>
         if ($.autoLock
-            && this.$box.closest('form').find('input[name=lockId]').val()
+            && this.$box.closest('form').find('input[name=lockCode]').val()
             && this.code.get()
         ) {
             $.autoLock.handleEvent();
diff --git a/scp/tickets.php b/scp/tickets.php
index 96b4ee51ba986c96f799630ba06d4477e11947e9..0a57e94c53c053b377504484ca5f80c98fa8a355 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -101,7 +101,7 @@ if($_POST && !$errors):
                 $response_form->getField('attachments')->reset();
 
                 // Remove staff's locks
-                $ticket->releaseLock();
+                $ticket->releaseLock($thisstaff->getId());
 
                 // Cleanup response draft for this user
                 Draft::deleteForNamespace(
@@ -182,7 +182,7 @@ if($_POST && !$errors):
                          $msg = __('Ticket is NOW assigned to you!');
                      } else {
                          $msg=sprintf(__('Ticket assigned successfully to %s'), $ticket->getAssigned());
-                         $ticket->releaseLock();
+                         $ticket->releaseLock($thisstaff->getId());
                          $ticket=null;
                      }
                  } elseif(!$errors['assign']) {