diff --git a/include/class.attachment.php b/include/class.attachment.php
index c0e9761ebba32cc69306562b790b20797f1d3b20..5efa5ad6d78fca490c13b2fb2b33c5811d0c3cd7 100644
--- a/include/class.attachment.php
+++ b/include/class.attachment.php
@@ -109,7 +109,7 @@ extends InstrumentedList {
     function keepOnlyFileIds($ids, $inline=false, $lang=false) {
         $new = array_fill_keys($ids, 1);
         foreach ($this as $A) {
-            if (!isset($new[$A->file_id]) && $A->lang == $lang)
+            if (!isset($new[$A->file_id]) && $A->lang == $lang && $A->inline == $inline)
                 // Not in the $ids list, delete
                 $this->remove($A);
             unset($new[$A->file_id]);
diff --git a/include/class.faq.php b/include/class.faq.php
index 75ef048d163f1c77adb85242487c513c3d6dd36d..372ee0f0d581031b04a8a293e58cc45e074074ab 100644
--- a/include/class.faq.php
+++ b/include/class.faq.php
@@ -291,7 +291,10 @@ class FAQ extends VerySimpleModel {
     }
 
     function getAttachments($lang=null) {
-        return $this->attachments->window(array('lang'=>$lang));
+        $att = $this->attachments;
+        if ($lang)
+            $att = $att->window(array('lang' => $lang));
+        return $att;
     }
 
     function getAttachmentsLinks($separator=' ',$target='') {
@@ -315,7 +318,7 @@ class FAQ extends VerySimpleModel {
         try {
             parent::delete();
             // Cleanup help topics.
-            db_query('DELETE FROM '.FAQ_TOPIC_TABLE.' WHERE faq_id='.db_input($this->getId()));
+            $this->topics->delete();
             // Cleanup attachments.
             $this->attachments->deleteAll();
         }
@@ -414,16 +417,12 @@ class FAQ extends VerySimpleModel {
         // ---------------------
         // Delete removed attachments.
         if (isset($vars['files'])) {
-            $keepers = $vars['files'];
-        }
-        else {
-            $keepers = array();
+            $this->getAttachments()->keepOnlyFileIds($vars['files'], false);
         }
 
         $images = Draft::getAttachmentIds($vars['answer']);
         $images = array_map(function($i) { return $i['id']; }, $images);
-        $keepers = array_merge($keepers, $images);
-        $this->getAttachments()->keepOnlyFileIds($keepers);
+        $this->getAttachments()->keepOnlyFileIds($images, true);
 
         // Handle language-specific attachments
         // ----------------------
@@ -439,7 +438,7 @@ class FAQ extends VerySimpleModel {
 
                 // FIXME: Include inline images in translated content
 
-                $this->getAttachments()->keepOnlyFileIds($keepers, false, $lang);
+                $this->getAttachments($lang)->keepOnlyFileIds($keepers, false, $lang);
             }
         }
 
diff --git a/include/class.orm.php b/include/class.orm.php
index 163e628b314908cb738f1d8b0f668eef69f00db6..adbf4829500ccbe6349dd537f83e7d3051586527 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -946,7 +946,13 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
     function constrain() {
         foreach (func_get_args() as $I) {
             foreach ($I as $path => $Q) {
-                $this->path_constraints[$path][] = $Q;
+                if (!is_array($Q) && !$Q instanceof Q) {
+                    // ->constrain(array('field__path__op' => val));
+                    $Q = array($path => $Q);
+                    list(, $path) = SqlCompiler::splitCriteria($path);
+                    $path = implode('__', $path);
+                }
+                $this->path_constraints[$path][] = $Q instanceof Q ? $Q : Q::all($Q);
             }
         }
         return $this;
@@ -2805,6 +2811,10 @@ class Q implements Serializable {
         return new static($constraints, self::ANY);
     }
 
+    static function all($constraints) {
+        return new static($constraints);
+    }
+
     function serialize() {
         return serialize(array(
             'f' =>
diff --git a/include/staff/faq-category.inc.php b/include/staff/faq-category.inc.php
index 9f2ff86dd3f548ab34a9064e18fabc8be2ab750c..e00d068e19458ff7b056b3f7a62ac4333c05fc7e 100644
--- a/include/staff/faq-category.inc.php
+++ b/include/staff/faq-category.inc.php
@@ -29,19 +29,18 @@ if ($thisstaff->hasPerm(FAQ::PERM_MANAGE)) {
 <?php
 }
 
-$sql='SELECT faq.faq_id, question, ispublished, count(attach.file_id) as attachments '
-    .' FROM '.FAQ_TABLE.' faq '
-    .' LEFT JOIN '.ATTACHMENT_TABLE.' attach
-         ON(attach.object_id=faq.faq_id AND attach.type=\'F\' AND attach.inline = 0) '
-    .' WHERE faq.category_id='.db_input($category->getId())
-    .' GROUP BY faq.faq_id ORDER BY question';
-if(($res=db_query($sql)) && db_num_rows($res)) {
+$faqs = $category->faqs
+    ->constrain(array('attachments__inline' => 0))
+    ->annotate(array('attachments' => SqlAggregate::COUNT('attachments')));
+if ($faqs->exists(true)) {
     echo '<div id="faq">
             <ol>';
-    while($row=db_fetch_array($res)) {
+    foreach ($faqs as $faq) {
         echo sprintf('
-            <li><a href="faq.php?id=%d" class="previewfaq">%s <span>- %s</span></a></li>',
-            $row['faq_id'],$row['question'],$row['ispublished']?__('Published'):__('Internal'));
+            <li><a href="faq.php?id=%d" class="previewfaq">%s <span>- %s</span></a> %s</li>',
+            $faq->getId(),$faq->getQuestion(),$faq->isPublished() ? __('Published'):__('Internal'),
+            $faq->attachments ? '<i class="icon-paperclip"></i>' : ''
+        );
     }
     echo '  </ol>
          </div>';
diff --git a/scp/canned.php b/scp/canned.php
index 8a54473754f103860f95c793059888b9e9a19193..06484ff7df50cf0eab3acf7c003973e4505ae128 100644
--- a/scp/canned.php
+++ b/scp/canned.php
@@ -45,21 +45,20 @@ if ($_POST) {
             } elseif($canned->update($_POST, $errors)) {
                 $msg=sprintf(__('Successfully updated %s'),
                     __('this canned response'));
+
                 //Delete removed attachments.
                 //XXX: files[] shouldn't be changed under any circumstances.
+                // Upload NEW attachments IF ANY - TODO: validate attachment types??
                 $keepers = $canned_form->getField('attachments')->getClean();
+                $canned->attachments->keepOnlyFileIds($keepers, false);
 
                 // Attach inline attachments from the editor
                 if (isset($_POST['draft_id'])
                         && ($draft = Draft::lookup($_POST['draft_id']))) {
                     $images = $draft->getAttachmentIds($_POST['response']);
-                    $images = array_map(function($i) { return $i['id']; }, $images);
-                    $keepers = array_merge($keepers, $images);
+                    $canned->attachments->keepOnlyFileIds($images, true);
                 }
 
-                // Upload NEW attachments IF ANY - TODO: validate attachment types??
-                $canned->attachments->keepOnlyFileIds($keepers);
-
                 // XXX: Handle nicely notifying a user that the draft was
                 // deleted | OR | show the draft for the user on the name
                 // page refresh or a nice bar popup immediately with
diff --git a/scp/faq.php b/scp/faq.php
index 7881dbc18edb42484653846f04a18ad9df66ea60..358a2afec6093d96776a80ccbc03bb33a07ed8eb 100644
--- a/scp/faq.php
+++ b/scp/faq.php
@@ -130,13 +130,13 @@ else {
         // Multi-lingual system
         foreach ($langs as $lang) {
             $attachments = $faq_form->getField('attachments.'.$lang);
-            $attachments->setAttachments($faq->getAttachments($lang));
+            $attachments->setAttachments($faq->getAttachments($lang)->window(array('inline' => false)));
         }
     }
     if ($faq) {
         // Common attachments
         $attachments = $faq_form->getField('attachments');
-        $attachments->setAttachments($faq->getAttachments());
+        $attachments->setAttachments($faq->getAttachments()->window(array('inline' => false)));
     }
 }