diff --git a/include/class.attachment.php b/include/class.attachment.php
index 1a34d79d79e6f73804a5ef861ab49007eef70073..d7f2d7f668cbd920cbc25dee19abf8364b5862ca 100644
--- a/include/class.attachment.php
+++ b/include/class.attachment.php
@@ -170,6 +170,7 @@ class GenericAttachments {
     function getInlines($lang=false) { return $this->_getList(false, true, $lang); }
     function getSeparates($lang=false) { return $this->_getList(true, false, $lang); }
     function getAll($lang=false) { return $this->_getList(true, true, $lang); }
+    function count($lang=false) { return count($this->getSeparates($lang)); }
 
     function _getList($separate=false, $inlines=false, $lang=false) {
         if(!isset($this->attachments)) {
diff --git a/include/class.category.php b/include/class.category.php
index 5f67c6ee5d182e1b837a4fdc8187f05d0bb40c66..41953b7ec46ad02ee26a247225e0452e7aeff230 100644
--- a/include/class.category.php
+++ b/include/class.category.php
@@ -13,156 +13,114 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Category {
-    var $id;
-    var $ht;
+class Category extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => FAQ_CATEGORY_TABLE,
+        'pk' => array('category_id'),
+        'ordering' => array('name'),
+        'joins' => array(
+            'faqs' => array(
+                'reverse' => 'FAQ.category_id'
+            ),
+        ),
+    );
 
-    function Category($id) {
-        $this->id=0;
-        $this->load($id);
-    }
+    /* ------------------> Getter methods <--------------------- */
+    function getId() { return $this->category_id; }
+    function getName() { return $this->name; }
+    function getNumFAQs() { return  $this->faqs->count(); }
+    function getDescription() { return $this->description; }
+    function getNotes() { return $this->notes; }
+    function getCreateDate() { return $this->created; }
+    function getUpdateDate() { return $this->updated; }
+
+    function isPublic() { return $this->ispublic; }
+    function getHashtable() { return $this->ht; }
 
-    function load($id) {
+    /* ------------------> Setter methods <--------------------- */
+    function setName($name) { $this->name=$name; }
+    function setNotes($notes) { $this->notes=$notes; }
+    function setDescription($desc) { $this->description=$desc; }
 
-        $sql=' SELECT cat.*,count(faq.faq_id) as faqs '
-            .' FROM '.FAQ_CATEGORY_TABLE.' cat '
-            .' LEFT JOIN '.FAQ_TABLE.' faq ON(faq.category_id=cat.category_id) '
-            .' WHERE cat.category_id='.db_input($id)
-            .' GROUP BY cat.category_id';
+    /* --------------> Database access methods <---------------- */
+    function update($vars, &$errors, $validation=false) {
 
-        if (!($res=db_query($sql)) || !db_num_rows($res)) 
-            return false;
+        // Cleanup.
+        $vars['name'] = Format::striptags(trim($vars['name']));
 
-        $this->ht = db_fetch_array($res);
-        $this->id = $this->ht['category_id'];
+        // Validate
+        if ($vars['id'] && $this->getId() != $vars['id'])
+            $errors['err'] = __('Internal error occurred');
 
-        return true;
-    }
+        if (!$vars['name'])
+            $errors['name'] = __('Category name is required');
+        elseif (strlen($vars['name']) < 3)
+            $errors['name'] = __('Name is too short. 3 chars minimum');
+        elseif (($cid=self::findIdByName($vars['name'])) && $cid != $vars['id'])
+            $errors['name'] = __('Category already exists');
 
-    function reload() {
-        return $this->load($this->getId());
-    }
+        if (!$vars['description'])
+            $errors['description'] = __('Category description is required');
 
-    /* ------------------> Getter methods <--------------------- */
-    function getId() { return $this->id; }
-    function getName() { return $this->ht['name']; }
-    function getNumFAQs() { return  $this->ht['faqs']; }
-    function getDescription() { return $this->ht['description']; }
-    function getNotes() { return $this->ht['notes']; }
-    function getCreateDate() { return $this->ht['created']; }
-    function getUpdateDate() { return $this->ht['updated']; }
-
-    function isPublic() { return ($this->ht['ispublic']); }
-    function getHashtable() { return $this->ht; }
-    
-    /* ------------------> Setter methods <--------------------- */
-    function setName($name) { $this->ht['name']=$name; }
-    function setNotes($notes) { $this->ht['notes']=$notes; }
-    function setDescription($desc) { $this->ht['description']=$desc; }
+        if ($errors)
+            return false;
 
-    /* --------------> Database access methods <---------------- */
-    function update($vars, &$errors) { 
+        /* validation only */
+        if ($validation)
+            return true;
 
-        if(!$this->save($this->getId(), $vars, $errors))
-            return false;
+        $this->ispublic = !!$vars['public'];
+        $this->name = $vars['name'];
+        $this->description = Format::sanitize($vars['description']);
+        $this->notes = Format::sanitize($vars['notes']);
 
-        //TODO: move FAQs if requested.
+        if (!$this->save())
+            return false;
 
-        $this->reload();
+        // TODO: Move FAQs if requested.
 
         return true;
     }
 
     function delete() {
-
-        $sql='DELETE FROM '.FAQ_CATEGORY_TABLE
-            .' WHERE category_id='.db_input($this->getId())
-            .' LIMIT 1';
-        if(db_query($sql) && ($num=db_affected_rows())) {
-            db_query('DELETE FROM '.FAQ_TABLE
-                    .' WHERE category_id='.db_input($this->getId()));
-    
+        try {
+            parent::delete();
+            $this->faqs->expunge();
         }
-
-        return $num;
-    }
-
-    /* ------------------> Static methods <--------------------- */
-
-    function lookup($id) {
-        return ($id && is_numeric($id) && ($c = new Category($id)))?$c:null;
+        catch (OrmException $e) {
+            return false;
+        }
+        return true;
     }
 
-    function findIdByName($name) {
-        $sql='SELECT category_id FROM '.FAQ_CATEGORY_TABLE.' WHERE name='.db_input($name);
-        list($id) = db_fetch_row(db_query($sql));
-
-        return $id;
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch);
     }
 
-    function findByName($name) {
-        if(($id=self::findIdByName($name)))
-            return new Category($id);
+    /* ------------------> Static methods <--------------------- */
 
-        return false;
-    }
+    static function findIdByName($name) {
+        $object = self::objects()->filter(array(
+            'name'=>$name
+        ))->values_flat('category_id')->one();
 
-    function validate($vars, &$errors) {
-         return self::save(0, $vars, $errors,true);
+        if ($object)
+            return $object[0];
     }
 
-    function create($vars, &$errors) {
-        return self::save(0, $vars, $errors);
+    static function findByName($name) {
+        return self::objects()->filter(array(
+            'name'=>$name
+        ))->one();
     }
 
-    function save($id, $vars, &$errors, $validation=false) {
-
-        //Cleanup.
-        $vars['name']=Format::striptags(trim($vars['name']));
-
-        //validate
-        if($id && $id!=$vars['id'])
-            $errors['err']=__('Internal error occurred');
-
-        if(!$vars['name'])
-            $errors['name']=__('Category name is required');
-        elseif(strlen($vars['name'])<3)
-            $errors['name']=__('Name is too short. 3 chars minimum');
-        elseif(($cid=self::findIdByName($vars['name'])) && $cid!=$id)
-            $errors['name']=__('Category already exists');
-
-        if(!$vars['description'])
-            $errors['description']=__('Category description is required');
-
-        if($errors) return false;
-
-        /* validation only */
-        if($validation) return true;
-
-        //save
-        $sql=' updated=NOW() '.
-             ',ispublic='.db_input(isset($vars['ispublic'])?$vars['ispublic']:0).
-             ',name='.db_input($vars['name']).
-             ',description='.db_input(Format::sanitize($vars['description'])).
-             ',notes='.db_input(Format::sanitize($vars['notes']));
-
-        if($id) {
-            $sql='UPDATE '.FAQ_CATEGORY_TABLE.' SET '.$sql.' WHERE category_id='.db_input($id);
-            if(db_query($sql))
-                return true;
-
-            $errors['err']=sprintf(__('Unable to update %s.'), __('this FAQ category'));
-
-        } else {
-            $sql='INSERT INTO '.FAQ_CATEGORY_TABLE.' SET '.$sql.',created=NOW()';
-            if(db_query($sql) && ($id=db_insert_id()))
-                return $id;
-
-            $errors['err']=sprintf(__('Unable to create %s.'), __('this FAQ category'))
-               .' '.__('Internal error occurred');
-        }
-
-        return false;
+    static function create($vars) {
+        $category = parent::create($vars);
+        $category->created = SqlFunction::NOW();
+        return $category;
     }
 }
 ?>
diff --git a/include/class.faq.php b/include/class.faq.php
index 75398d5e29fedbcd9683e485f27aeba1decf52ec..83b4f8053072f1ae30e599ef317e6dae4f15fc9e 100644
--- a/include/class.faq.php
+++ b/include/class.faq.php
@@ -15,81 +15,58 @@
 require_once('class.file.php');
 require_once('class.category.php');
 
-class FAQ {
+class FAQ extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => FAQ_TABLE,
+        'pk' => array('faq_id'),
+        'ordering' => array('question'),
+        'joins' => array(
+            'category' => array(
+                'constraint' => array(
+                    'category_id' => 'Category.category_id'
+                ),
+            ),
+        ),
+    );
 
-    var $id;
-    var $ht;
-
-    var $category;
     var $attachments;
+    var $topics;
 
-    function FAQ($id) {
-        $this->id=0;
-        $this->ht = array();
-        $this->load($id);
-    }
-
-    function load($id) {
-
-        $sql='SELECT faq.*,cat.ispublic, count(attach.file_id) as attachments '
-            .' FROM '.FAQ_TABLE.' faq '
-            .' LEFT JOIN '.FAQ_CATEGORY_TABLE.' cat ON(cat.category_id=faq.category_id) '
-            .' LEFT JOIN '.ATTACHMENT_TABLE.' attach
-                 ON(attach.object_id=faq.faq_id AND attach.`type`=\'F\' AND attach.inline=0) '
-            .' WHERE faq.faq_id='.db_input($id)
-            .' GROUP BY faq.faq_id';
-
-        if (!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht = db_fetch_array($res);
-        $this->ht['id'] = $this->id = $this->ht['faq_id'];
-        $this->category = null;
-        $this->attachments = new GenericAttachments($this->id, 'F');
-
-        return true;
-    }
-
-    function reload() {
-        return $this->load($this->getId());
+    function __construct() {
+        call_user_func_array(array('parent', '__construct'), func_get_args());
+        $this->attachments = new GenericAttachments($this->getId(), 'F');
     }
 
     /* ------------------> Getter methods <--------------------- */
-    function getId() { return $this->id; }
+    function getId() { return $this->faq_id; }
     function getHashtable() { return $this->ht; }
-    function getKeywords() { return $this->ht['keywords']; }
-    function getQuestion() { return $this->ht['question']; }
-    function getAnswer() { return $this->ht['answer']; }
+    function getKeywords() { return $this->keywords; }
+    function getQuestion() { return $this->question; }
+    function getAnswer() { return $this->answer; }
     function getAnswerWithImages() {
-        return Format::viewableImages($this->ht['answer'], ROOT_PATH.'image.php');
+        return Format::viewableImages($this->answer, ROOT_PATH.'image.php');
     }
     function getSearchableAnswer() {
-        return ThreadBody::fromFormattedText($this->ht['answer'], 'html')
+        return ThreadBody::fromFormattedText($this->answer, 'html')
             ->getSearchable();
     }
-    function getNotes() { return $this->ht['notes']; }
-    function getNumAttachments() { return $this->ht['attachments']; }
-
-    function isPublished() { return (!!$this->ht['ispublished'] && !!$this->ht['ispublic']); }
+    function getNotes() { return $this->notes; }
+    function getNumAttachments() { return $this->attachments->count(); }
 
-    function getCreateDate() { return $this->ht['created']; }
-    function getUpdateDate() { return $this->ht['updated']; }
+    function isPublished() { return (!!$this->ispublished && !!$this->category->ispublic); }
 
-    function getCategoryId() { return $this->ht['category_id']; }
-    function getCategory() {
-        if(!$this->category && $this->getCategoryId())
-            $this->category = Category::lookup($this->getCategoryId());
+    function getCreateDate() { return $this->created; }
+    function getUpdateDate() { return $this->updated; }
 
-        return $this->category;
-    }
+    function getCategoryId() { return $this->category_id; }
+    function getCategory() { return $this->category; }
 
     function getHelpTopicsIds() {
-
-        if (!isset($this->ht['topics']) && ($topics=$this->getHelpTopics())) {
-            $this->ht['topics'] = array_keys($topics);
+        if ($topics=$this->getHelpTopics()) {
+            return array_keys($topics);
         }
-
-        return $this->ht['topics'];
+        return array();
     }
 
     function getHelpTopics() {
@@ -98,7 +75,7 @@ class FAQ {
         if (!isset($this->topics)) {
             $this->topics = array();
             $sql='SELECT t.topic_id, CONCAT_WS(" / ", pt.topic, t.topic) as name  FROM '.TOPIC_TABLE.' t '
-                .' INNER JOIN '.FAQ_TOPIC_TABLE.' ft ON(ft.topic_id=t.topic_id AND ft.faq_id='.db_input($this->id).') '
+                .' INNER JOIN '.FAQ_TOPIC_TABLE.' ft ON(ft.topic_id=t.topic_id AND ft.faq_id='.db_input($this->getId()).') '
                 .' LEFT JOIN '.TOPIC_TABLE.' pt ON(pt.topic_id=t.topic_pid) '
                 .' ORDER BY t.topic';
             if (($res=db_query($sql)) && db_num_rows($res)) {
@@ -111,11 +88,11 @@ class FAQ {
     }
 
     /* ------------------> Setter methods <--------------------- */
-    function setPublished($val) { $this->ht['ispublished'] = !!$val; }
-    function setQuestion($question) { $this->ht['question'] = Format::striptags(trim($question)); }
-    function setAnswer($text) { $this->ht['answer'] = $text; }
-    function setKeywords($words) { $this->ht['keywords'] = $words; }
-    function setNotes($text) { $this->ht['notes'] = $text; }
+    function setPublished($val) { $this->ispublished = !!$val; }
+    function setQuestion($question) { $this->question = Format::striptags(trim($question)); }
+    function setAnswer($text) { $this->answer = $text; }
+    function setKeywords($words) { $this->keywords = $words; }
+    function setNotes($text) { $this->notes = $text; }
 
     /* For ->attach() and ->detach(), use $this->attachments() (nolint) */
     function attach($file) { return $this->_attachments->add($file); }
@@ -123,27 +100,18 @@ class FAQ {
 
     function publish() {
         $this->setPublished(1);
-
-        return $this->apply();
+        return $this->save();
     }
 
     function unpublish() {
         $this->setPublished(0);
-
-        return $this->apply();
-    }
-
-    /* Same as update - but mainly called after one or more setters are changed. */
-    function apply() {
-        $errors = array();
-        //XXX: set errors and add ->getErrors() & ->getError()
-        return $this->update($this->ht, $errors);
+        return $this->save();
     }
 
     // Internationalization of the knowledge base
 
     function getTranslateTag($subtag) {
-        return _H(sprintf('faq.%s.%s', $subtag, $this->id));
+        return _H(sprintf('faq.%s.%s', $subtag, $this->getId()));
     }
     function getLocal($subtag) {
         $tag = $this->getTranslateTag($subtag);
@@ -177,6 +145,7 @@ class FAQ {
         }
         return $this->ht[$what];
     }
+
     function getLocalAttachments($lang=false) {
         if (!$lang) {
             $lang = Internationalization::getCurrentLanguage();
@@ -207,66 +176,6 @@ class FAQ {
         Signal::send('model.updated', $this);
     }
 
-    function update($vars, &$errors) {
-        global $cfg;
-
-        if (!$this->save($this->getId(), $vars, $errors))
-            return false;
-
-        $this->updateTopics($vars['topics']);
-
-        // General attachments (for all languages)
-        // ---------------------
-        // Delete removed attachments.
-        if (isset($vars['files'])) {
-            $keepers = $vars['files'];
-            if (($attachments = $this->attachments->getSeparates())) {
-                foreach($attachments as $file) {
-                    if($file['id'] && !in_array($file['id'], $keepers))
-                        $this->attachments->delete($file['id']);
-                }
-            }
-        }
-        // Upload new attachments IF any.
-        $this->attachments->upload($keepers);
-
-        // Handle language-specific attachments
-        // ----------------------
-        $langs = $cfg ? $cfg->getSecondaryLanguages() : false;
-        if ($langs) {
-            $langs[] = $cfg->getPrimaryLanguage();
-            foreach ($langs as $lang) {
-                if (!isset($vars['files_'.$lang]))
-                    // Not updating the FAQ
-                    continue;
-
-                $keepers = $vars['files_'.$lang];
-
-                // Delete removed attachments.
-                if (($attachments = $this->attachments->getSeparates($lang))) {
-                    foreach ($attachments as $file) {
-                        if ($file['id'] && !in_array($file['id'], $keepers))
-                            $this->attachments->delete($file['id']);
-                    }
-                }
-                // Upload new attachments IF any.
-                $this->attachments->upload($keepers, false, $lang);
-            }
-        }
-
-        // Inline images (attached to the draft)
-        $this->attachments->deleteInlines();
-        $this->attachments->upload(Draft::getAttachmentIds($vars['answer']));
-
-        if (isset($vars['trans']) && !$this->saveTranslations($vars))
-            return false;
-
-        $this->reload();
-
-        Signal::send('model.updated', $this);
-        return true;
-    }
-
     function saveTranslations($vars) {
         global $thisstaff;
 
@@ -335,127 +244,144 @@ class FAQ {
     }
 
     function delete() {
-
-        $sql='DELETE FROM '.FAQ_TABLE
-            .' WHERE faq_id='.db_input($this->getId())
-            .' LIMIT 1';
-        if(!db_query($sql) || !db_affected_rows())
+        try {
+            parent::delete();
+            // Cleanup help topics.
+            db_query('DELETE FROM '.FAQ_TOPIC_TABLE.' WHERE faq_id='.db_input($this->getId()));
+            // Cleanup attachments.
+            $this->attachments->deleteAll();
+        }
+        catch (OrmException $ex) {
             return false;
-
-        //Cleanup help topics.
-        db_query('DELETE FROM '.FAQ_TOPIC_TABLE.' WHERE faq_id='.db_input($this->id));
-        //Cleanup attachments.
-        $this->attachments->deleteAll();
-
+        }
         return true;
     }
 
     /* ------------------> Static methods <--------------------- */
 
-    function add($vars, &$errors) {
-        if(!($id=self::create($vars, $errors)))
+    static function add($vars, &$errors) {
+        if(!($faq = self::create($vars)))
             return false;
 
-        if(($faq=self::lookup($id))) {
-            $faq->updateTopics($vars['topics']);
-
-            if($_FILES['attachments'] && ($files=AttachmentFile::format($_FILES['attachments'])))
-                $faq->attachments->upload($files);
-
-            // Inline images (attached to the draft)
-            if (isset($vars['draft_id']) && $vars['draft_id'])
-                if ($draft = Draft::lookup($vars['draft_id']))
-                    $faq->attachments->upload($draft->getAttachmentIds(), true);
-
-            $faq->reload();
-        }
-
         return $faq;
     }
 
-    function create($vars, &$errors) {
-        return self::save(0, $vars, $errors);
+    static function create($vars) {
+        $faq = parent::create($vars);
+        $faq->created = SqlFunction::NOW();
+        return $faq;
     }
 
-    function lookup($id) {
-        return ($id && is_numeric($id) && ($obj= new FAQ($id)) && $obj->getId()==$id)? $obj : null;
+    static function countPublishedFAQs() {
+        return self::objects()->filter(array(
+            'category__ispublic' => true,
+            'ispublished'=> true
+        ))->count();
     }
 
-    function countPublishedFAQs() {
-        $sql='SELECT count(faq.faq_id) '
-            .' FROM '.FAQ_TABLE.' faq '
-            .' INNER JOIN '.FAQ_CATEGORY_TABLE.' cat ON(cat.category_id=faq.category_id AND cat.ispublic=1) '
-            .' WHERE faq.ispublished=1';
+    static function findIdByQuestion($question) {
+        $object = self::objects()->filter(array(
+            'question'=>$question
+        ))->values_flat('faq_id')->one();
 
-        return db_result(db_query($sql));
+        if ($object)
+            return $object[0];
     }
 
-    function findIdByQuestion($question) {
-        $sql='SELECT faq_id FROM '.FAQ_TABLE
-            .' WHERE question='.db_input($question);
-
-        list($id) =db_fetch_row(db_query($sql));
-
-        return $id;
+    static function findByQuestion($question) {
+        return self::objects()->filter(array(
+            'question'=>$question
+        ))->one();
     }
 
-    function findByQuestion($question) {
-
-        if(($id=self::findIdByQuestion($question)))
-            return self::lookup($id);
-
-        return false;
-    }
-
-    function save($id, $vars, &$errors, $validation=false) {
-
-        //Cleanup.
-        $vars['question']=Format::striptags(trim($vars['question']));
+    function update($vars, &$errors) {
+        global $cfg;
 
-        //validate
-        if($id && $id!=$vars['id'])
-            $errors['err'] = __('Internal error. Try again');
+        // Cleanup.
+        $vars['question'] = Format::striptags(trim($vars['question']));
 
-        if(!$vars['question'])
+        // Validate
+        if ($vars['id'] && $this->getId() != $vars['id'])
+            $errors['err'] = __('Internal error occurred');
+        elseif (!$vars['question'])
             $errors['question'] = __('Question required');
-        elseif(($qid=self::findIdByQuestion($vars['question'])) && $qid!=$id)
+        elseif (($qid=self::findIdByQuestion($vars['question'])) && $qid != $vars['id'])
             $errors['question'] = __('Question already exists');
 
-        if(!$vars['category_id'] || !($category=Category::lookup($vars['category_id'])))
+        if (!$vars['category_id'] || !($category=Category::lookup($vars['category_id'])))
             $errors['category_id'] = __('Category is required');
 
-        if(!$vars['answer'])
+        if (!$vars['answer'])
             $errors['answer'] = __('FAQ answer is required');
 
-        if($errors || $validation) return (!$errors);
+        if ($errors)
+            return false;
 
-        //save
-        $sql=' updated=NOW() '
-            .', question='.db_input($vars['question'])
-            .', answer='.db_input(Format::sanitize($vars['answer'], false))
-            .', category_id='.db_input($vars['category_id'])
-            .', ispublished='.db_input(isset($vars['ispublished'])?$vars['ispublished']:0)
-            .', notes='.db_input(Format::sanitize($vars['notes']));
+        $this->question = $vars['question'];
+        $this->answer = Format::sanitize($vars['answer']);
+        $this->category = $category;
+        $this->ispublished = !!$vars['ispublished'];
+        $this->notes = Format::sanitize($vars['notes']);
 
-        if($id) {
-            $sql='UPDATE '.FAQ_TABLE.' SET '.$sql.' WHERE faq_id='.db_input($id);
-            if(db_query($sql))
-                return true;
+        if (!$this->save())
+            return false;
 
-            $errors['err']=sprintf(__('Unable to update %s.'), __('this FAQ article'));
+        $this->updateTopics($vars['topics']);
 
-        } else {
-            $sql='INSERT INTO '.FAQ_TABLE.' SET '.$sql.',created=NOW()';
-            if (db_query($sql) && ($id=db_insert_id())) {
-                Signal::send('model.created', FAQ::lookup($id));
-                return $id;
+        // General attachments (for all languages)
+        // ---------------------
+        // Delete removed attachments.
+        if (isset($vars['files'])) {
+            $keepers = $vars['files'];
+            if (($attachments = $this->attachments->getSeparates())) {
+                foreach($attachments as $file) {
+                    if($file['id'] && !in_array($file['id'], $keepers))
+                        $this->attachments->delete($file['id']);
+                }
             }
+        }
+        // Upload new attachments IF any.
+        $this->attachments->upload($keepers);
+
+        // Handle language-specific attachments
+        // ----------------------
+        $langs = $cfg ? $cfg->getSecondaryLanguages() : false;
+        if ($langs) {
+            $langs[] = $cfg->getPrimaryLanguage();
+            foreach ($langs as $lang) {
+                if (!isset($vars['files_'.$lang]))
+                    // Not updating the FAQ
+                    continue;
 
-            $errors['err']=sprintf(__('Unable to create %s.'), __('this FAQ article'))
-               .' '.__('Internal error occurred');
+                $keepers = $vars['files_'.$lang];
+
+                // Delete removed attachments.
+                if (($attachments = $this->attachments->getSeparates($lang))) {
+                    foreach ($attachments as $file) {
+                        if ($file['id'] && !in_array($file['id'], $keepers))
+                            $this->attachments->delete($file['id']);
+                    }
+                }
+                // Upload new attachments IF any.
+                $this->attachments->upload($keepers, false, $lang);
+            }
         }
 
-        return false;
+        // Inline images (attached to the draft)
+        $this->attachments->deleteInlines();
+        $this->attachments->upload(Draft::getAttachmentIds($vars['answer']));
+
+        if (isset($vars['trans']) && !$this->saveTranslations($vars))
+            return false;
+
+        return true;
+    }
+
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
     }
 }
+FAQ::_inspect();
 ?>
diff --git a/include/staff/faq.inc.php b/include/staff/faq.inc.php
index 4a9b7fb46c48dc271e1de021b2923a0425c670fd..e5337ec0b702103aa9a198d31823411ea9f93d5f 100644
--- a/include/staff/faq.inc.php
+++ b/include/staff/faq.inc.php
@@ -113,7 +113,7 @@ if ($topics = Topic::getAllHelpTopics()) {
 <ul class="tabs" style="margin-top:9px;">
     <li class="active"><a href="#article"><?php echo __('Article Content'); ?></a></li>
     <li><a href="#attachments"><?php echo __('Attachments') . sprintf(' (%d)',
-        count($faq->attachments->getSeparates(''))); ?></a></li>
+        $faq ? count($faq->attachments->getSeparates('')) : 0); ?></a></li>
     <li><a href="#notes"><?php echo __('Internal Notes'); ?></a></li>
 </ul>
 
@@ -150,7 +150,7 @@ if ($faq) { ?>
         $aname = 'answer';
     }
     else {
-        $namespace = $faq->getId() . $code;
+        $namespace = $faq ? $faq->getId() . $code : $code;
         $answer = $info['trans'][$code]['answer'];
         $question = $info['trans'][$code]['question'];
         $qname = 'trans['.$code.'][question]';
diff --git a/scp/categories.php b/scp/categories.php
index 8afdc4c0f47da5ee7d9a52341fe509a9718bea71..422ec184135fe7a1dc6af185d02aa3a8b6a135cb 100644
--- a/scp/categories.php
+++ b/scp/categories.php
@@ -40,7 +40,8 @@ if($_POST){
             }
             break;
         case 'create':
-            if(($id=Category::create($_POST,$errors))) {
+            $category = Category::create();
+            if ($category->update($_POST, $errors)) {
                 $msg=sprintf(__('Successfull added %s'), Format::htmlchars($_POST['name']));
                 $_REQUEST['a']=null;
             } elseif(!$errors['err']) {
@@ -55,11 +56,13 @@ if($_POST){
                 $count=count($_POST['ids']);
                 switch(strtolower($_POST['a'])) {
                     case 'make_public':
-                        $sql='UPDATE '.FAQ_CATEGORY_TABLE.' SET ispublic=1 '
-                            .' WHERE category_id IN ('.implode(',', db_input($_POST['ids'])).')';
-
-                        if(db_query($sql) && ($num=db_affected_rows())) {
-                            if($num==$count)
+                        $num = Category::objects()->filter(array(
+                            'category_id__in'=>$_POST['ids']
+                        ))->update(array(
+                            'ispublic'=>true
+                        ));
+                        if ($num > 0) {
+                            if ($num==$count)
                                 $msg = sprintf(__('Successfully made %s PUBLIC'),
                                     _N('selected category', 'selected categories', $count));
                             else
@@ -71,10 +74,12 @@ if($_POST){
                         }
                         break;
                     case 'make_private':
-                        $sql='UPDATE '.FAQ_CATEGORY_TABLE.' SET ispublic=0 '
-                            .' WHERE category_id IN ('.implode(',', db_input($_POST['ids'])).')';
-
-                        if(db_query($sql) && ($num=db_affected_rows())) {
+                        $num = Category::objects()->filter(array(
+                            'category_id__in'=>$_POST['ids']
+                        ))->update(array(
+                            'ispublic'=>false
+                        ));
+                        if ($num > 0) {
                             if($num==$count)
                                 $msg = sprintf(__('Successfully made %s PRIVATE'),
                                     _N('selected category', 'selected categories', $count));
@@ -87,19 +92,17 @@ if($_POST){
                         }
                         break;
                     case 'delete':
-                        $i=0;
-                        foreach($_POST['ids'] as $k=>$v) {
-                            if(($c=Category::lookup($v)) && $c->delete())
-                                $i++;
-                        }
+                        $i = Category::objects()->filter(array(
+                            'category_id__in'=>$_POST['ids']
+                        ))->delete();
 
-                        if($i==$count)
+                        if ($i==$count)
                             $msg = sprintf(__('Successfully deleted %s'),
                                 _N('selected category', 'selected categories', $count));
-                        elseif($i>0)
+                        elseif ($i > 0)
                             $warn = sprintf(__('%1$d of %2$d %3$s deleted'), $i, $count,
                                 _N('selected category', 'selected categories', $count));
-                        elseif(!$errors['err'])
+                        elseif (!$errors['err'])
                             $errors['err'] = sprintf(__('Unable to delete %s'),
                                 _N('selected category', 'selected categories', $count));
                         break;
diff --git a/scp/faq.php b/scp/faq.php
index 868cebbae341973d6b8b15072b4c94744f07d24e..ea4b72f7c31526cf184d2d55ac6c56d4725fe4f2 100644
--- a/scp/faq.php
+++ b/scp/faq.php
@@ -62,7 +62,8 @@ if ($_POST) {
     switch(strtolower($_POST['do'])) {
         case 'create':
         case 'add':
-            if(($faq=FAQ::add($_POST,$errors))) {
+            $faq = FAQ::create();
+            if($faq->update($_POST,$errors)) {
                 $msg=sprintf(__('Successfully added %s'), Format::htmlchars($faq->getQuestion()));
                 // Delete draft for this new faq
                 Draft::deleteForNamespace('faq', $thisstaff->getId());
@@ -77,7 +78,6 @@ if ($_POST) {
             elseif($faq->update($_POST,$errors)) {
                 $msg=sprintf(__('Successfully updated %s'), __('this FAQ article'));
                 $_REQUEST['a']=null; //Go back to view
-                $faq->reload();
                 // Delete pending draft updates for this faq (for ALL users)
                 Draft::deleteForNamespace('faq.'.$faq->getId());
             } elseif(!$errors['err'])