From 666264879c59c553f7898c41457b64e89eac2afe Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Thu, 24 Oct 2013 10:05:51 +1100
Subject: [PATCH] change it so all topics MUST include a category, we store a
 special uncategorized category to compensate this cleans up a bunch of
 internals and removes some settings

---
 .../discourse/components/utilities.js         |  3 +-
 .../controllers/edit_category_controller.js   | 44 ++++-----------
 .../discourse/helpers/application_helpers.js  |  3 +-
 .../javascripts/discourse/models/category.js  | 13 -----
 .../discourse/models/category_list.js         | 15 +----
 .../templates/featured_topics.js.handlebars   |  2 +-
 .../list/wide_categories.js.handlebars        |  2 +-
 .../discourse/views/category_chooser_view.js  |  4 +-
 app/controllers/list_controller.rb            | 25 ++-------
 app/models/category.rb                        | 11 ++--
 app/models/category_list.rb                   | 56 +------------------
 app/models/post.rb                            |  2 +-
 app/models/site_setting.rb                    |  8 +--
 app/models/topic.rb                           | 45 +++++++++------
 .../category_detailed_serializer.rb           |  2 +-
 app/serializers/site_serializer.rb            |  8 +--
 config/locales/client.cs.yml                  |  1 -
 config/locales/client.de.yml                  |  1 -
 config/locales/client.en.yml                  |  1 -
 config/locales/client.fr.yml                  |  1 -
 config/locales/client.it.yml                  |  1 -
 config/locales/client.ko.yml                  |  1 -
 config/locales/client.nb_NO.yml               |  1 -
 config/locales/client.nl.yml                  |  1 -
 config/locales/client.pseudo.yml              |  1 -
 config/locales/client.pt_BR.yml               |  1 -
 config/locales/client.ru.yml                  |  1 -
 config/locales/client.zh_CN.yml               |  1 -
 config/locales/client.zh_TW.yml               |  1 -
 config/locales/server.cs.yml                  |  4 --
 config/locales/server.da.yml                  |  1 -
 config/locales/server.de.yml                  |  4 --
 config/locales/server.en.yml                  |  4 --
 config/locales/server.es.yml                  |  1 -
 config/locales/server.fr.yml                  |  3 -
 config/locales/server.id.yml                  |  1 -
 config/locales/server.it.yml                  |  4 --
 config/locales/server.ko.yml                  |  4 --
 config/locales/server.nl.yml                  |  4 --
 config/locales/server.pseudo.yml              |  6 --
 config/locales/server.pt.yml                  |  1 -
 config/locales/server.pt_BR.yml               |  4 --
 config/locales/server.ru.yml                  |  3 -
 config/locales/server.sv.yml                  |  2 -
 config/locales/server.zh_CN.yml               |  2 -
 config/locales/server.zh_TW.yml               |  2 -
 ...131022045114_add_uncategorized_category.rb | 35 ++++++++++++
 lib/post_creator.rb                           |  1 -
 lib/site_setting_extension.rb                 | 29 ++++++----
 lib/site_settings/local_process_provider.rb   | 10 +++-
 lib/topic_query.rb                            | 16 +-----
 spec/components/category_list_spec.rb         | 36 ++----------
 spec/components/guardian_spec.rb              |  7 ++-
 spec/components/topic_query_spec.rb           |  6 +-
 spec/components/trashable_spec.rb             |  8 +--
 .../controllers/categories_controller_spec.rb |  2 +-
 spec/controllers/list_controller_spec.rb      | 34 +----------
 spec/fabricators/topic_fabricator.rb          |  1 +
 spec/models/category_spec.rb                  | 31 +++++-----
 spec/models/post_mover_spec.rb                |  2 +-
 spec/models/site_spec.rb                      | 10 +++-
 spec/models/topic_spec.rb                     | 13 ++---
 spec/spec_helper.rb                           |  5 ++
 63 files changed, 183 insertions(+), 369 deletions(-)
 create mode 100644 db/migrate/20131022045114_add_uncategorized_category.rb

diff --git a/app/assets/javascripts/discourse/components/utilities.js b/app/assets/javascripts/discourse/components/utilities.js
index 3b930ac7b..eef2ed1e2 100644
--- a/app/assets/javascripts/discourse/components/utilities.js
+++ b/app/assets/javascripts/discourse/components/utilities.js
@@ -40,8 +40,9 @@ Discourse.Utilities = {
     @param {Discourse.Category} category the category whose link we want
     @returns {String} the html category badge
   **/
-  categoryLink: function(category) {
+  categoryLink: function(category, allowUncategorized) {
     if (!category) return "";
+    if (!allowUncategorized && Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id")) return "";
 
     var color = Em.get(category, 'color'),
         textColor = Em.get(category, 'text_color'),
diff --git a/app/assets/javascripts/discourse/controllers/edit_category_controller.js b/app/assets/javascripts/discourse/controllers/edit_category_controller.js
index 838ac0226..0686254af 100644
--- a/app/assets/javascripts/discourse/controllers/edit_category_controller.js
+++ b/app/assets/javascripts/discourse/controllers/edit_category_controller.js
@@ -30,9 +30,6 @@ Discourse.EditCategoryController = Discourse.ObjectController.extend(Discourse.M
     if (this.get('id')) {
       return I18n.t("category.edit_long") + " : " + this.get('model.name');
     }
-    if (this.get('isUncategorized')){
-      return I18n.t("category.edit_uncategorized");
-    }
     return I18n.t("category.create") + (this.get('model.name') ? (" : " + this.get('model.name')) : '');
   }.property('id', 'model.name'),
 
@@ -127,37 +124,16 @@ Discourse.EditCategoryController = Discourse.ObjectController.extend(Discourse.M
     saveCategory: function() {
       var categoryController = this;
       this.set('saving', true);
-
-
-      if( this.get('isUncategorized') ) {
-        $.when(
-          Discourse.SiteSetting.update('uncategorized_color', this.get('color')),
-          Discourse.SiteSetting.update('uncategorized_text_color', this.get('text_color')),
-          Discourse.SiteSetting.update('uncategorized_name', this.get('name'))
-        ).then(function(result) {
-          // success
-          categoryController.send('closeModal');
-          // We can't redirect to the uncategorized category on save because the slug
-          // might have changed.
-          Discourse.URL.redirectTo("/categories");
-        }, function(errors) {
-          // errors
-          if(errors.length === 0) errors.push(I18n.t("category.save_error"));
-          categoryController.displayErrors(errors);
-          categoryController.set('saving', false);
-        });
-      } else {
-        this.get('model').save().then(function(result) {
-          // success
-          categoryController.send('closeModal');
-          Discourse.URL.redirectTo("/category/" + Discourse.Category.slugFor(result.category));
-        }, function(errors) {
-          // errors
-          if(errors.length === 0) errors.push(I18n.t("category.creation_error"));
-          categoryController.displayErrors(errors);
-          categoryController.set('saving', false);
-        });
-      }
+      this.get('model').save().then(function(result) {
+        // success
+        categoryController.send('closeModal');
+        Discourse.URL.redirectTo("/category/" + Discourse.Category.slugFor(result.category));
+      }, function(errors) {
+        // errors
+        if(errors.length === 0) errors.push(I18n.t("category.creation_error"));
+        categoryController.displayErrors(errors);
+        categoryController.set('saving', false);
+      });
     },
 
     deleteCategory: function() {
diff --git a/app/assets/javascripts/discourse/helpers/application_helpers.js b/app/assets/javascripts/discourse/helpers/application_helpers.js
index 89a58dc90..e1c58b29d 100644
--- a/app/assets/javascripts/discourse/helpers/application_helpers.js
+++ b/app/assets/javascripts/discourse/helpers/application_helpers.js
@@ -42,8 +42,9 @@ Handlebars.registerHelper('topicLink', function(property, options) {
   @for Handlebars
 **/
 Handlebars.registerHelper('categoryLink', function(property, options) {
+  var allowUncategorized = options.hash && options.hash.allowUncategorized;
   var category = Ember.Handlebars.get(this, property, options);
-  return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category));
+  return new Handlebars.SafeString(Discourse.Utilities.categoryLink(category, allowUncategorized));
 });
 
 
diff --git a/app/assets/javascripts/discourse/models/category.js b/app/assets/javascripts/discourse/models/category.js
index 793404853..499df75d3 100644
--- a/app/assets/javascripts/discourse/models/category.js
+++ b/app/assets/javascripts/discourse/models/category.js
@@ -117,19 +117,6 @@ Discourse.Category = Discourse.Model.extend({
 
 Discourse.Category.reopenClass({
 
-  uncategorizedInstance: function() {
-    if (this.uncategorized) return this.uncategorized;
-
-    this.uncategorized = this.create({
-      slug: 'uncategorized',
-      name: Discourse.SiteSettings.uncategorized_name,
-      isUncategorized: true,
-      color: Discourse.SiteSettings.uncategorized_color,
-      text_color: Discourse.SiteSettings.uncategorized_text_color
-    });
-    return this.uncategorized;
-  },
-
   slugFor: function(category) {
     if (!category) return "";
 
diff --git a/app/assets/javascripts/discourse/models/category_list.js b/app/assets/javascripts/discourse/models/category_list.js
index 24761a1c5..1f4b36af4 100644
--- a/app/assets/javascripts/discourse/models/category_list.js
+++ b/app/assets/javascripts/discourse/models/category_list.js
@@ -45,19 +45,8 @@ Discourse.CategoryList.reopenClass({
         });
       }
 
-      if (c.is_uncategorized) {
-        var uncategorized = Discourse.Category.uncategorizedInstance();
-        uncategorized.setProperties({
-          topics: c.topics,
-          featured_users: c.featured_users,
-          topics_week: c.topics_week,
-          topics_month: c.topics_month,
-          topics_year: c.topics_year
-        });
-        categories.pushObject(uncategorized);
-      } else {
-        categories.pushObject(Discourse.Category.create(c));
-      }
+      categories.pushObject(Discourse.Category.create(c));
+
     });
     return categories;
   },
diff --git a/app/assets/javascripts/discourse/templates/featured_topics.js.handlebars b/app/assets/javascripts/discourse/templates/featured_topics.js.handlebars
index 38c078350..4963365d3 100644
--- a/app/assets/javascripts/discourse/templates/featured_topics.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/featured_topics.js.handlebars
@@ -1,7 +1,7 @@
 <table id='topic-list'>
   <tr>
     <th class="main-link">
-      {{categoryLink this}}
+      {{categoryLink this allowUncategorized=true}}
 
       <div class='posters'>
       {{#each featured_users}}
diff --git a/app/assets/javascripts/discourse/templates/list/wide_categories.js.handlebars b/app/assets/javascripts/discourse/templates/list/wide_categories.js.handlebars
index f1e499e2f..444d63133 100644
--- a/app/assets/javascripts/discourse/templates/list/wide_categories.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/list/wide_categories.js.handlebars
@@ -20,7 +20,7 @@
           {{#if controller.ordering}}
             <i class="icon-reorder"></i>
           {{/if}}
-          {{categoryLink this}}
+          {{categoryLink this allowUncategorized=true}}
           {{#if unreadTopics}}
             <a href={{unbound url}} class='badge new-posts badge-notification' title='{{i18n topic.unread_topics count="unreadTopics"}}'>{{unbound unreadTopics}}</a>
           {{/if}}
diff --git a/app/assets/javascripts/discourse/views/category_chooser_view.js b/app/assets/javascripts/discourse/views/category_chooser_view.js
index aba5c5afa..c74488426 100644
--- a/app/assets/javascripts/discourse/views/category_chooser_view.js
+++ b/app/assets/javascripts/discourse/views/category_chooser_view.js
@@ -15,8 +15,10 @@ Discourse.CategoryChooserView = Discourse.ComboboxView.extend({
   init: function() {
     this._super();
     // TODO perhaps allow passing a param in to select if we need full or not
+
+    var uncategorized_id = Discourse.Site.currentProp("uncategorized_category_id");
     this.set('content', _.filter(Discourse.Category.list(), function(c){
-      return c.permission === Discourse.PermissionType.FULL;
+      return c.permission === Discourse.PermissionType.FULL && c.id !== uncategorized_id;
     }));
   },
 
diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb
index d9146bf59..07b15a5b7 100644
--- a/app/controllers/list_controller.rb
+++ b/app/controllers/list_controller.rb
@@ -49,18 +49,13 @@ class ListController < ApplicationController
   def category
     query = TopicQuery.new(current_user, page: params[:page])
 
-    # If they choose uncategorized, return topics NOT in a category
-    if request_is_for_uncategorized?
-      list = query.list_uncategorized
-    else
-      if !@category
-        raise Discourse::NotFound
-        return
-      end
-      guardian.ensure_can_see!(@category)
-      list = query.list_category(@category)
-      @description = @category.description
+    if !@category
+      raise Discourse::NotFound
+      return
     end
+    guardian.ensure_can_see!(@category)
+    list = query.list_category(@category)
+    @description = @category.description
 
     if params[:parent_category].present?
       list.more_topics_url = url_for(category_list_parent_path(params[:parent_category], params[:category], page: next_page, format: "json"))
@@ -72,8 +67,6 @@ class ListController < ApplicationController
   end
 
   def category_feed
-    raise Discourse::InvalidParameters.new('Category RSS of "uncategorized"') if request_is_for_uncategorized?
-
     guardian.ensure_can_see!(@category)
     discourse_expires_in 1.minute
 
@@ -137,12 +130,6 @@ class ListController < ApplicationController
                 Category.where(id: slug.to_i, parent_category_id: parent_category_id).includes(:featured_users).first
   end
 
-  def request_is_for_uncategorized?
-    params[:category] == Slug.for(SiteSetting.uncategorized_name) ||
-      params[:category] == SiteSetting.uncategorized_name ||
-      params[:category] == 'uncategorized'
-  end
-
   def build_topic_list_options
     # html format means we need to parse exclude category (aka filter) from the site options top menu
     menu_items = SiteSetting.top_menu_items
diff --git a/app/models/category.rb b/app/models/category.rb
index b0f322ca6..dd45d05d8 100644
--- a/app/models/category.rb
+++ b/app/models/category.rb
@@ -117,7 +117,7 @@ class Category < ActiveRecord::Base
 
     topics_with_post_count = Topic
                               .select("topics.category_id, COUNT(*) topic_count, SUM(topics.posts_count) post_count")
-                              .where("topics.id NOT IN (select cc.topic_id from categories cc)")
+                              .where("topics.id NOT IN (select cc.topic_id from categories cc WHERE topic_id IS NOT NULL)")
                               .group("topics.category_id")
                               .visible.to_sql
 
@@ -150,10 +150,11 @@ SQL
   end
 
   def create_category_definition
-    create_topic!(title: I18n.t("category.topic_prefix", category: name), user: user, pinned_at: Time.now)
-    update_column(:topic_id, topic.id)
-    topic.update_column(:category_id, id)
-    topic.posts.create(raw: post_template, user: user)
+    t = Topic.new(title: I18n.t("category.topic_prefix", category: name), user: user, pinned_at: Time.now, category_id: id)
+    t.skip_callbacks = true
+    t.save!
+    update_column(:topic_id, t.id)
+    t.posts.create(raw: post_template, user: user)
   end
 
   def topic_url
diff --git a/app/models/category_list.rb b/app/models/category_list.rb
index 04013cd45..9d9185e79 100644
--- a/app/models/category_list.rb
+++ b/app/models/category_list.rb
@@ -16,7 +16,6 @@ class CategoryList
     find_categories
 
     prune_empty
-    add_uncategorized
     find_user_data
   end
 
@@ -91,63 +90,14 @@ class CategoryList
       end
     end
 
-    # Add the uncategorized "magic" category
-    # TODO: remove this entire hack, not needed
-    def add_uncategorized
-      # Support for uncategorized topics
-      uncategorized_topics = Topic
-                        .listable_topics
-                        .visible
-                        .where(category_id: nil)
-                        .topic_list_order
-                        .limit(SiteSetting.category_featured_topics)
-      if uncategorized_topics.present?
-
-        totals = Topic.exec_sql("SELECT SUM(CASE WHEN created_at >= (CURRENT_TIMESTAMP - INTERVAL '1 WEEK') THEN 1 ELSE 0 END) as topics_week,
-                                        SUM(CASE WHEN created_at >= (CURRENT_TIMESTAMP - INTERVAL '1 MONTH') THEN 1 ELSE 0 END) as topics_month,
-                                        SUM(CASE WHEN created_at >= (CURRENT_TIMESTAMP - INTERVAL '1 YEAR') THEN 1 ELSE 0 END) as topics_year,
-                                        COUNT(*) AS topic_count
-                                 FROM topics
-                                 WHERE topics.visible
-                                  AND topics.deleted_at IS NULL
-                                  AND topics.category_id IS NULL
-                                  AND topics.archetype <> '#{Archetype.private_message}'").first
-
-
-        uncategorized = Category.new({name: SiteSetting.uncategorized_name,
-                                      slug: Slug.for(SiteSetting.uncategorized_name),
-                                      color: SiteSetting.uncategorized_color,
-                                      text_color: SiteSetting.uncategorized_text_color,
-                                      featured_topics: uncategorized_topics}.merge(totals))
-
-        # Find the appropriate place to insert it:
-        insert_at = nil
-
-        unless latest_post_only?
-          @categories.each_with_index do |c, idx|
-            if (uncategorized.topics_week || 0) > (c.topics_week || 0)
-              insert_at = idx
-              break
-            end
-          end
-        end
-
-        @categories.insert(insert_at || @categories.size, uncategorized)
-      end
-
-      if uncategorized.present?
-        @all_topics ||= []
-        uncategorized.displayable_topics = uncategorized_topics
-        @all_topics << uncategorized_topics
-        @all_topics.flatten!
-      end
-    end
 
     # Remove any empty topics unless we can create them (so we can see the controls)
     def prune_empty
       unless @guardian.can_create?(Category)
         # Remove categories with no featured topics unless we have the ability to edit one
-        @categories.delete_if { |c| c.displayable_topics.blank? && c.description.nil? }
+        @categories.delete_if { |c|
+          c.displayable_topics.blank? && c.description.blank?
+        }
       end
     end
 
diff --git a/app/models/post.rb b/app/models/post.rb
index d1bfd3e6f..8e3f4d1c8 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -84,7 +84,7 @@ class Post < ActiveRecord::Base
     super
     update_flagged_posts_count
     TopicLink.extract_from(self)
-    if topic && topic.category_id
+    if topic && topic.category_id && topic.category
       topic.category.update_latest
     end
   end
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index 800126207..fa6e8a0c0 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -103,10 +103,6 @@ class SiteSetting < ActiveRecord::Base
   setting(:max_mentions_per_post, 10)
   setting(:newuser_max_mentions_per_post, 2)
 
-  client_setting(:uncategorized_name, 'uncategorized')
-  client_setting(:uncategorized_color, 'AB9364');
-  client_setting(:uncategorized_text_color, 'FFFFFF');
-
   setting(:unique_posts_mins, Rails.env.test? ? 0 : 5)
 
   # Rate Limits
@@ -267,11 +263,13 @@ class SiteSetting < ActiveRecord::Base
   setting(:max_daily_gravatar_crawls, 500)
 
   setting(:sequential_replies_threshold, 2)
-
   client_setting(:enable_mobile_theme, true)
 
   setting(:dominating_topic_minimum_percent, 20)
 
+  # hidden setting only used by system
+  hidden_setting(:uncategorized_category_id, -1, hidden: true)
+
   def self.call_discourse_hub?
     self.enforce_global_nicknames? && self.discourse_org_access_key.present?
   end
diff --git a/app/models/topic.rb b/app/models/topic.rb
index 0df39d989..619afa123 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -56,11 +56,12 @@ class Topic < ActiveRecord::Base
                                         :case_sensitive => false,
                                         :collection => Proc.new{ Topic.listable_topics } }
 
-  # The allow_uncategorized_topics site setting can be changed at any time, so there may be
-  # existing topics with nil category. We'll allow that, but when someone tries to make a new
-  # topic or change a topic's category, perform validation.
-  attr_accessor :do_category_validation
-  validates :category_id, :presence => { :if => Proc.new { @do_category_validation && !SiteSetting.allow_uncategorized_topics } }
+  validates :category_id, :presence => true ,:exclusion => {:in => [SiteSetting.uncategorized_category_id]},
+                                     :if => Proc.new { |t|
+                                           (t.new_record? || t.category_id_changed?) &&
+                                           !SiteSetting.allow_uncategorized_topics
+                                       }
+
 
   before_validation do
     self.sanitize_title
@@ -142,13 +143,16 @@ class Topic < ActiveRecord::Base
   before_create do
     self.bumped_at ||= Time.now
     self.last_post_user_id ||= user_id
-    self.do_category_validation = true
     if !@ignore_category_auto_close and self.category and self.category.auto_close_days and self.auto_close_at.nil?
       set_auto_close(self.category.auto_close_days)
     end
   end
 
+  attr_accessor :skip_callbacks
+
   after_create do
+    return if skip_callbacks
+
     changed_to_category(category)
     if archetype == Archetype.private_message
       DraftSequence.next!(user, Draft::NEW_PRIVATE_MESSAGE)
@@ -158,14 +162,21 @@ class Topic < ActiveRecord::Base
   end
 
   before_save do
+    return if skip_callbacks
+
     if (auto_close_at_changed? and !auto_close_at_was.nil?) or (auto_close_user_id_changed? and auto_close_at)
       self.auto_close_started_at ||= Time.zone.now if auto_close_at
       Jobs.cancel_scheduled_job(:close_topic, {topic_id: id})
       true
     end
+    if category_id.nil? && (archetype.nil? || archetype == "regular")
+      self.category_id = SiteSetting.uncategorized_category_id
+    end
   end
 
   after_save do
+    return if skip_callbacks
+
     if auto_close_at and (auto_close_at_changed? or auto_close_user_id_changed?)
       Jobs.enqueue_at(auto_close_at, :close_topic, {topic_id: id, user_id: auto_close_user_id || user_id})
     end
@@ -333,8 +344,13 @@ class Topic < ActiveRecord::Base
         Category.where(['id = ?', category_id]).update_all 'topic_count = topic_count - 1'
       end
 
-      self.category_id = cat.id
-      if save
+      success = true
+      if self.category_id != cat.id
+        self.category_id = cat.id
+        success = save
+      end
+
+      if success
         CategoryFeaturedTopic.feature_topics_for(old_category)
         Category.where(id: cat.id).update_all 'topic_count = topic_count + 1'
         CategoryFeaturedTopic.feature_topics_for(cat) unless old_category.try(:id) == cat.try(:id)
@@ -372,20 +388,15 @@ class Topic < ActiveRecord::Base
 
   # Changes the category to a new name
   def change_category(name)
-    self.do_category_validation = true
-
     # If the category name is blank, reset the attribute
     if name.blank?
-      if category_id.present?
-        CategoryFeaturedTopic.feature_topics_for(category)
-        Category.where(id: category_id).update_all 'topic_count = topic_count - 1'
-      end
-      self.category_id = nil
-      return save
+      cat = Category.where(id: SiteSetting.uncategorized_category_id).first
+    else
+      cat = Category.where(name: name).first
     end
 
-    cat = Category.where(name: name).first
     return true if cat == category
+    return false unless cat
     changed_to_category(cat)
   end
 
diff --git a/app/serializers/category_detailed_serializer.rb b/app/serializers/category_detailed_serializer.rb
index b4b31b217..af695d350 100644
--- a/app/serializers/category_detailed_serializer.rb
+++ b/app/serializers/category_detailed_serializer.rb
@@ -23,7 +23,7 @@ class CategoryDetailedSerializer < BasicCategorySerializer
   end
 
   def is_uncategorized
-    name == SiteSetting.uncategorized_name
+    object.id == SiteSetting.uncategorized_category_id
   end
 
   def include_is_uncategorized?
diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb
index d67f65c4e..2fe85fef2 100644
--- a/app/serializers/site_serializer.rb
+++ b/app/serializers/site_serializer.rb
@@ -3,8 +3,8 @@ class SiteSerializer < ApplicationSerializer
   attributes :default_archetype,
              :notification_types,
              :post_types,
-             :uncategorized_slug,
-             :group_names
+             :group_names,
+             :uncategorized_category_id # this is hidden so putting it here
 
 
   has_many :categories, serializer: BasicCategorySerializer, embed: :objects
@@ -21,8 +21,8 @@ class SiteSerializer < ApplicationSerializer
     Post.types
   end
 
-  def uncategorized_slug
-    Slug.for(SiteSetting.uncategorized_name)
+  def uncategorized_category_id
+    SiteSetting.uncategorized_category_id
   end
 
 end
diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml
index ac23b2ce0..7a9df917e 100644
--- a/config/locales/client.cs.yml
+++ b/config/locales/client.cs.yml
@@ -935,7 +935,6 @@ cs:
       none: '(bez kategorie)'
       edit: 'upravit'
       edit_long: "Upravit kategorii"
-      edit_uncategorized: "Upravit nekategorizované"
       view: 'Zobrazit témata v kategorii'
       general: 'Obecné'
       settings: 'Nastavení'
diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml
index f901e7d48..4e25f34e5 100644
--- a/config/locales/client.de.yml
+++ b/config/locales/client.de.yml
@@ -925,7 +925,6 @@ de:
       none: '(keine Kategorie)'
       edit: 'Bearbeiten'
       edit_long: "Kategorie bearbeiten"
-      edit_uncategorized: "Unkategorisierte bearbeiten"
       view: 'Zeige Themen dieser Kategorie'
       general: 'Generell'
       settings: 'Einstellungen'
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 6ebb86129..74c9266bc 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -945,7 +945,6 @@ en:
       choose: 'Select a category&hellip;'
       edit: 'edit'
       edit_long: "Edit Category"
-      edit_uncategorized: "Edit Uncategorized"
       view: 'View Topics in Category'
       general: 'General'
       settings: 'Settings'
diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml
index c0bcfb1a0..a373c82f4 100644
--- a/config/locales/client.fr.yml
+++ b/config/locales/client.fr.yml
@@ -831,7 +831,6 @@ fr:
       choose: 'Sélectionner une catégorie&hellip;'
       edit: 'éditer'
       edit_long: "Editer la catégorie"
-      edit_uncategorized: "Editer les sans catégorie"
       view: 'Voir les discussions dans cette catégorie'
       general: 'Général'
       settings: 'Paramètres'
diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml
index 4c2016e33..0210be809 100644
--- a/config/locales/client.it.yml
+++ b/config/locales/client.it.yml
@@ -885,7 +885,6 @@ it:
       none: '(nessuna categoria)'
       edit: 'modifica'
       edit_long: "Modifica Categoria"
-      edit_uncategorized: "Modifica Non categorizzata"
       view: 'Mostra Topic nella Categoria'
       general: 'Generale'
       settings: 'Impostazioni'
diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml
index df6e36b56..d3179c216 100644
--- a/config/locales/client.ko.yml
+++ b/config/locales/client.ko.yml
@@ -733,7 +733,6 @@ ko:
       none: '(카테고리 없음)'
       edit: '편집'
       edit_long: "카테고리 편집"
-      edit_uncategorized: "분류되지 않은 편집"
       view: '카테고리 항목보기'
       delete: '카테고리 지우기'
       create: '카테고리 만들기'
diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml
index 94db1dfe1..ea9aa4f78 100644
--- a/config/locales/client.nb_NO.yml
+++ b/config/locales/client.nb_NO.yml
@@ -797,7 +797,6 @@ nb_NO:
       none: '(no category)'
       edit: 'rediger'
       edit_long: "Rediger Kategori"
-      edit_uncategorized: "Rediger Ukategorisert"
       view: 'Se Emner i Kategori'
       general: 'Generellt'
       settings: 'Innstillinger'
diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml
index 3723aaa97..5ec41bb05 100644
--- a/config/locales/client.nl.yml
+++ b/config/locales/client.nl.yml
@@ -890,7 +890,6 @@ nl:
       none: (geen categorie)
       edit: bewerk
       edit_long: Bewerk categorie
-      edit_uncategorized: "Wijzig ongecategoriseerd"
       view: Bekijk topics in categorie
       general: Algemeen
       settings: Instellingen
diff --git a/config/locales/client.pseudo.yml b/config/locales/client.pseudo.yml
index 027e4a95f..62d1143e5 100644
--- a/config/locales/client.pseudo.yml
+++ b/config/locales/client.pseudo.yml
@@ -851,7 +851,6 @@ pseudo:
       none: '[[ (ɳó čáťéǧóřý) ]]'
       edit: '[[ éďíť ]]'
       edit_long: '[[ Éďíť Čáťéǧóřý ]]'
-      edit_uncategorized: '[[ Éďíť Ůɳčáťéǧóřížéď ]]'
       view: '[[ Ѷíéŵ Ťóƿíčš íɳ Čáťéǧóřý ]]'
       general: '[[ Ǧéɳéřáł ]]'
       settings: '[[ Šéťťíɳǧš ]]'
diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml
index bb96ab792..6f1054292 100644
--- a/config/locales/client.pt_BR.yml
+++ b/config/locales/client.pt_BR.yml
@@ -944,7 +944,6 @@ pt_BR:
       choose: 'Selecione uma categoria&hellip;'
       edit: 'editar'
       edit_long: "Editar Categoria"
-      edit_uncategorized: "Editar Sem Categoria"
       view: 'Visualizar Tópicos na Categoria'
       general: 'Geral'
       settings: 'Configurações'
diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml
index dbdf521af..3be78a6d7 100644
--- a/config/locales/client.ru.yml
+++ b/config/locales/client.ru.yml
@@ -942,7 +942,6 @@ ru:
       choose: 'Select a category&hellip;'
       edit: изменить
       edit_long: 'Изменить категорию'
-      edit_uncategorized: 'Изменить "Без категории"'
       view: 'Просмотр тем по категориям'
       general: Общие
       settings: Настройки
diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml
index d00c75270..c9d67090e 100644
--- a/config/locales/client.zh_CN.yml
+++ b/config/locales/client.zh_CN.yml
@@ -945,7 +945,6 @@ zh_CN:
       choose: '选择分类……'
       edit: '编辑'
       edit_long: "编辑分类"
-      edit_uncategorized: "编辑未分类的"
       view: '浏览分类下的主题'
       general: '通常'
       settings: '设置'
diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml
index a938eea40..560f1fe53 100644
--- a/config/locales/client.zh_TW.yml
+++ b/config/locales/client.zh_TW.yml
@@ -844,7 +844,6 @@ zh_TW:
       none: '(未分類)'
       edit: '編輯'
       edit_long: "編輯分類"
-      edit_uncategorized: "編輯未分類的"
       view: '浏覽分類下的主題'
       general: '通常'
       settings: '設置'
diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml
index 3919deb65..b8a6648db 100644
--- a/config/locales/server.cs.yml
+++ b/config/locales/server.cs.yml
@@ -596,10 +596,6 @@ cs:
     active_user_rate_limit_secs: "Jak často aktualizujeme informaci o poslední návštěvě uživatelů, v sekundách"
     previous_visit_timeout_hours: "Kolik času musí uplynout v hodinách, než je návštěva uživatele považována za uplynulou"
 
-    uncategorized_name: "Výchozí kategorie pro témata, která nemají nastavenou kategorii"
-    uncategorized_color: "Barva pozadí štítku kategorie pro témata, která nemají nastavenou kategorii"
-    uncategorized_text_color: "Barva textu na štítku kategorie pro témata, která nemají nastavenou kategorii"
-
     rate_limit_create_topic: "Počet sekund, které je nutné počkat od vytvoření tématu, než smí uživatel vytvořit další"
     rate_limit_create_post: "Počet sekund, které je nutné počkat od zaslání příspěvku, než smí uživatel zaslat další"
 
diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml
index 5dfbecd0e..7dbced667 100644
--- a/config/locales/server.da.yml
+++ b/config/locales/server.da.yml
@@ -385,7 +385,6 @@ da:
     active_user_rate_limit_secs: "How frequently we update the 'last_seen_at' field, in seconds"
     previous_visit_timeout_hours: "How long a visit lasts before we consider it the 'previous' visit, in hours"
 
-    uncategorized_name: "The default category for topics that have no category in the /categories page"
     max_mentions_per_post: "Maximum number of @name notifications you can use in a single post"
 
     rate_limit_create_topic: "How many seconds, after creating a topic, before you can create another topic"
diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml
index 5c6a088b6..888cf2bb8 100644
--- a/config/locales/server.de.yml
+++ b/config/locales/server.de.yml
@@ -570,10 +570,6 @@ de:
     active_user_rate_limit_secs: "Sekunden, nach denen das 'last_seen_at'-Feld aktualisiert wird."
     previous_visit_timeout_hours: "Stunden, die ein Besuch dauert bevor er als 'früherer' Besuch gezählt wird."
 
-    uncategorized_name: "Name der Standardkategorie für Themen, die keiner Kategorie zugeordnet wurden in der Kategorieübersicht /categories."
-    uncategorized_color: "Die Hintergrundfarbe der Plakette für die Kategorie der Themen welche keine Kategorie haben"
-    uncategorized_text_color: "Die Textfarbe der Plakette für die Kategorie der Themen welche keine Kategorie haben"
-
     rate_limit_create_topic: "Sekunden Wartezeit nach Erstellung eines Themas, bevor man ein neues Thema erstellen kann."
     rate_limit_create_post: "Sekunden Wartezeit nach Erstellung eines Beitrags, bevor man einen neuen Beitrag erstellen kann."
 
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 9f6cec0fd..e928e5daf 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -607,10 +607,6 @@ en:
     active_user_rate_limit_secs: "How frequently we update the 'last_seen_at' field, in seconds"
     previous_visit_timeout_hours: "How long a visit lasts before we consider it the 'previous' visit, in hours"
 
-    uncategorized_name: "The default category for topics that have no category in the /categories page"
-    uncategorized_color: "The background color of the uncategorized topics category"
-    uncategorized_text_color: "The text color of the uncategorized topics category"
-
     rate_limit_create_topic: "After creating a topic, users must wait this many seconds before they can create another topic"
     rate_limit_create_post: "After posting, users must wait this many seconds before they can create another post"
 
diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml
index 1f0b97a5a..11dbcddd2 100644
--- a/config/locales/server.es.yml
+++ b/config/locales/server.es.yml
@@ -373,7 +373,6 @@ es:
     active_user_rate_limit_secs: "Como de frecuentemente actualizaremos el campo 'last_seen_at', en segundos"
     previous_visit_timeout_hours: "Cuanto tiempo debe pasar antes de que una visita sea considerada la 'visita previa', en horas"
 
-    uncategorized_name: "La categoría por defecto para los topics que no tienen categoría en la página /categories"
     max_mentions_per_post: "Maximo número de notificaciones @name que se pueden usar en un post"
 
     rate_limit_create_topic: "Cuantos segundos, después de crear un topic, deben pasar antes de poder crear otro topic"
diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml
index c8501fc25..223e75f06 100644
--- a/config/locales/server.fr.yml
+++ b/config/locales/server.fr.yml
@@ -547,9 +547,6 @@ fr:
     allow_import: "Autoriser l'importation qui remplacera TOUTES les données du site. Laisser non coché, sauf si vous prévoyez d'importer des données."
     active_user_rate_limit_secs: "A quelle fréquence mettre à jour le champ 'dernier_vu_a', en secondes."
     previous_visit_timeout_hours: "Combien de temps dure une visite avant de la considérer comme la visite 'précédente', en heures."
-    uncategorized_name: "Catégorie par défaut pour les discussions sans catégorie sur la pages /categories"
-    uncategorized_color: "La couleur de fond de la catégorie des discussions sans catégorie"
-    uncategorized_text_color: "La couleur du texte de la catégorie des discussions sans catégorie"
     rate_limit_create_topic: "Après la création d'une discussion, les utilisateurs doivent attendre ce nombre de secondes avant de pouvoir créer une nouvelle discussion"
     rate_limit_create_post: "Après avoir posté un message, les utilisateurs doivent attendre ce nombre de secondes avant de pouvoir en poster un nouveau"
     max_likes_per_day: "Quantité maximale de J'aime qu'un utilisateur peut effectuer en un jour"
diff --git a/config/locales/server.id.yml b/config/locales/server.id.yml
index 2b6dfd00c..0ef901e28 100644
--- a/config/locales/server.id.yml
+++ b/config/locales/server.id.yml
@@ -384,7 +384,6 @@ id:
     active_user_rate_limit_secs: "How frequently we update the 'last_seen_at' field, in seconds"
     previous_visit_timeout_hours: "How long a visit lasts before we consider it the 'previous' visit, in hours"
 
-    uncategorized_name: "The default category for topics that have no category in the /categories page"
     max_mentions_per_post: "Maximum number of @name notifications you can use in a single post"
 
     rate_limit_create_topic: "How many seconds, after creating a topic, before you can create another topic"
diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml
index ef70f655e..cb150a790 100644
--- a/config/locales/server.it.yml
+++ b/config/locales/server.it.yml
@@ -506,10 +506,6 @@ it:
     active_user_rate_limit_secs: "Quanto frequentemente viene aggiornato il campo 'last_seen_at' field, in secondi"
     previous_visit_timeout_hours: "Durata di una visita prima che venga considerata la visita 'precedente', in ore"
 
-    uncategorized_name: "La categoria di default per i topic non categorizzati nella pagina /categories"
-    uncategorized_color: "Il colore di sfondo del badge per la categoria con i topic privi di categoria"
-    uncategorized_text_color: "Il colore del testo del badge per la categoria con i topic privi di categoria"
-
     rate_limit_create_topic: "Quanti secondi, dopo aver creato un topic, per poter creare un nuovo topic"
     rate_limit_create_post: "Quanti secondi, dopo aver creato un post, per poter creare un nuovo post"
 
diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml
index 1fdd99e83..4f8ab54a2 100644
--- a/config/locales/server.ko.yml
+++ b/config/locales/server.ko.yml
@@ -506,10 +506,6 @@ ko:
     active_user_rate_limit_secs: "How frequently we update the 'last_seen_at' field, in seconds"
     previous_visit_timeout_hours: "How long a visit lasts before we consider it the 'previous' visit, in hours"
 
-    uncategorized_name: "The default category for topics that have no category in the /categories page"
-    uncategorized_color: "The background color of the badge for the category with topics that have no category"
-    uncategorized_text_color: "The text color of the badge for the category with topics that have no category"
-
     rate_limit_create_topic: "How many seconds, after creating a topic, before you can create another topic"
     rate_limit_create_post: "How many seconds, after creating a post, before you can create another post"
 
diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml
index e4fdbac8f..edac72a3f 100644
--- a/config/locales/server.nl.yml
+++ b/config/locales/server.nl.yml
@@ -570,10 +570,6 @@ nl:
     active_user_rate_limit_secs: "Hoe vaak we het 'last_seen_at'-veld updaten, in seconden."
     previous_visit_timeout_hours: "Hoe lang een bezoek duurt voordat we het als het 'vorige' bezoek beschouwen, in uren."
 
-    uncategorized_name: De naam voor ongecategoriseerde topics in de categorielijst
-    uncategorized_color: De achtergrondkleur van de badge voor de categorie met topics zonder categorie
-    uncategorized_text_color: De tekstkleur van de badge voor de categorie met topics zonder categorie
-
     rate_limit_create_topic: Hoeveel seconden voordat je een ander topic kan aanmaken
     rate_limit_create_post: Hoeveel seconden voordat je een ander bericht kan aanmaken
 
diff --git a/config/locales/server.pseudo.yml b/config/locales/server.pseudo.yml
index a24e1ec58..521b3e10c 100644
--- a/config/locales/server.pseudo.yml
+++ b/config/locales/server.pseudo.yml
@@ -685,12 +685,6 @@ pseudo:
       ƒíéłď, íɳ šéčóɳďš ]]'
     previous_visit_timeout_hours: '[[ Ĥóŵ łóɳǧ á νíšíť łášťš ƀéƒóřé ŵé čóɳšíďéř íť
       ťĥé ''ƿřéνíóůš'' νíšíť, íɳ ĥóůřš ]]'
-    uncategorized_name: '[[ Ťĥé ďéƒáůłť čáťéǧóřý ƒóř ťóƿíčš ťĥáť ĥáνé ɳó čáťéǧóřý
-      íɳ ťĥé /čáťéǧóříéš ƿáǧé ]]'
-    uncategorized_color: '[[ Ťĥé ƀáčǩǧřóůɳď čółóř óƒ ťĥé ƀáďǧé ƒóř ťĥé čáťéǧóřý ŵíťĥ
-      ťóƿíčš ťĥáť ĥáνé ɳó čáťéǧóřý ]]'
-    uncategorized_text_color: '[[ Ťĥé ťéхť čółóř óƒ ťĥé ƀáďǧé ƒóř ťĥé čáťéǧóřý ŵíťĥ
-      ťóƿíčš ťĥáť ĥáνé ɳó čáťéǧóřý ]]'
     rate_limit_create_topic: '[[ Ĥóŵ ɱáɳý šéčóɳďš, áƒťéř čřéáťíɳǧ á ťóƿíč, ƀéƒóřé
       ýóů čáɳ čřéáťé áɳóťĥéř ťóƿíč ]]'
     rate_limit_create_post: '[[ Ĥóŵ ɱáɳý šéčóɳďš, áƒťéř čřéáťíɳǧ á ƿóšť, ƀéƒóřé ýóů
diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml
index 9267a851c..405673266 100644
--- a/config/locales/server.pt.yml
+++ b/config/locales/server.pt.yml
@@ -313,7 +313,6 @@ pt:
     active_user_rate_limit_secs: "How frequently we update the 'last_seen_at' field, in seconds."
     previous_visit_timeout_hours: "How long a visit lasts before we consider it the 'previous' visit, in hours."
 
-    uncategorized_name: "The name for the uncategorized topics on the category list"
     max_mentions_per_post: "The maximum amount of @notifications you can add to a post"
 
     rate_limit_create_topic: "How many seconds before you can create another topic"
diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml
index 51f27661d..28f026a18 100644
--- a/config/locales/server.pt_BR.yml
+++ b/config/locales/server.pt_BR.yml
@@ -601,10 +601,6 @@ pt_BR:
     active_user_rate_limit_secs: "Qual a frequencia de atualização do campo 'última vez visto em', em segundos."
     previous_visit_timeout_hours: "Quanto tempo uma visita dura antes de considerarmos como 'última visita', em horas."
 
-    uncategorized_name: "Nome para tópicos sem categoria na lista de categorias"
-    uncategorized_color: "Cor de fundo da etiqueta da categoria que tem tópicos sem nenhuma categoria"
-    uncategorized_text_color: "Cor do texto da etiqueta da categoria que tem tópicos sem nenhuma categoria"
-
     rate_limit_create_topic: "Quantos segundos você precisa aguardar antes de poder criar um novo tópico"
     rate_limit_create_post: "Quantos segundos você precisa aguardar antes de poder fazer uma nova postagem"
 
diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml
index ddfc0b385..557046da7 100644
--- a/config/locales/server.ru.yml
+++ b/config/locales/server.ru.yml
@@ -568,9 +568,6 @@ ru:
     allow_import: 'Позволить импорт, который может заменить ВСЕ данные сайта. Оставьте false, если не планируете импортировать данные'
     active_user_rate_limit_secs: 'Как часто мы обновляем поле ''last_seen_at'', в секундах'
     previous_visit_timeout_hours: 'Как долго должно длиться посещение сайта, чтобы мы посчитали его «предыдущим посещением», в часах'
-    uncategorized_name: 'Категория по умолчанию для тем, которые не отнесены ни к одной категории на странице /categories'
-    uncategorized_color: 'Фоновый цвет категории с темами без установленной категории'
-    uncategorized_text_color: 'Цвет текста категории с темами без установленной категории'
     rate_limit_create_topic: 'После создания темы пользователи должны выждать указанное количество секунд перед созданием новой темы'
     rate_limit_create_post: 'После создания сообщения пользователи должны выждать указанное количество секунд перед созданием нового сообщения'
     max_likes_per_day: 'Максимальное количество симпатий, выраженных одним пользователем в день'
diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml
index ed9bf4c30..4794ce069 100644
--- a/config/locales/server.sv.yml
+++ b/config/locales/server.sv.yml
@@ -428,8 +428,6 @@ sv:
     active_user_rate_limit_secs: "How frequently we update the 'last_seen_at' field, in seconds"
     previous_visit_timeout_hours: "How long a visit lasts before we consider it the 'previous' visit, in hours"
 
-    uncategorized_name: "The default category for topics that have no category in the /categories page"
-
     rate_limit_create_topic: "How many seconds, after creating a topic, before you can create another topic"
     rate_limit_create_post: "How many seconds, after creating a post, before you can create another post"
 
diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml
index 3d1ab05b2..5b6ac26ac 100644
--- a/config/locales/server.zh_CN.yml
+++ b/config/locales/server.zh_CN.yml
@@ -525,8 +525,6 @@ zh_CN:
     active_user_rate_limit_secs: "更新“最后一次见到”数据的频率,单位为秒"
     previous_visit_timeout_hours: "系统判断一次访问之后多少小时后为“上一次”访问"
 
-    uncategorized_name: "在分类 /categories 页面,没有分类的主题的缺省分类"
-
     rate_limit_create_topic: "在创建一个主题之后间隔多少秒你才能创建另一个主题"
     rate_limit_create_post: "在创建一个帖子之后间隔多少秒你才能创建另一个帖子"
 
diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml
index 485a6f4de..2e3c53b6b 100644
--- a/config/locales/server.zh_TW.yml
+++ b/config/locales/server.zh_TW.yml
@@ -506,8 +506,6 @@ zh_TW:
     active_user_rate_limit_secs: "更新“最後一次見到”數據的頻率,單位爲秒"
     previous_visit_timeout_hours: "系統判斷一次訪問之後多少小時後爲“上一次”訪問"
 
-    uncategorized_name: "在分類 /categories 頁面,沒有分類的主題的缺省分類"
-
     rate_limit_create_topic: "在創建一個主題之後間隔多少秒你才能創建另一個主題"
     rate_limit_create_post: "在創建一個帖子之後間隔多少秒你才能創建另一個帖子"
 
diff --git a/db/migrate/20131022045114_add_uncategorized_category.rb b/db/migrate/20131022045114_add_uncategorized_category.rb
new file mode 100644
index 000000000..d8416d916
--- /dev/null
+++ b/db/migrate/20131022045114_add_uncategorized_category.rb
@@ -0,0 +1,35 @@
+class AddUncategorizedCategory < ActiveRecord::Migration
+  def up
+
+    result = execute "SELECT 1 FROM categories WHERE name = 'uncategorized'"
+    if result.count > 0
+      name << SecureRandom.hex
+    end
+
+
+    result = execute "INSERT INTO categories
+            (name,color,slug,description,text_color, user_id, created_at, updated_at, position)
+     VALUES ('uncategorized', 'AB9364', 'uncategorized', '', 'FFFFFF', -1, now(), now(), 1 )
+     RETURNING id
+    "
+    category_id = result[0]["id"].to_i
+
+    execute "INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
+             VALUES ('uncategorized_category_id', 3, #{category_id}, now(), now())"
+
+
+    execute "DELETE from site_settings where name in ('uncategorized_name', 'uncategorized_text_color', 'uncategorized_color')"
+
+    execute "UPDATE topics SET category_id = #{category_id} WHERE archetype = 'regular' AND category_id IS NULL"
+
+    execute "ALTER table topics ADD CONSTRAINT has_category_id CHECK (category_id IS NOT NULL OR archetype <> 'regular')"
+
+  end
+
+  def down
+    execute "ALTER TABLE topics DROP CONSTRAINT has_category_id"
+    execute "DELETE from categories WHERE id in (select value::int from site_settings where name = 'uncategorized_category_id')"
+    execute "DELETE from site_settings where name = 'uncategorized_category_id'"
+    execute "UPDATE topics SET category_id = null WHERE category_id NOT IN (SELECT id from categories)"
+  end
+end
diff --git a/lib/post_creator.rb b/lib/post_creator.rb
index 1279048d8..559c3e8d9 100644
--- a/lib/post_creator.rb
+++ b/lib/post_creator.rb
@@ -140,7 +140,6 @@ class PostCreator
   end
 
   def after_topic_create
-
     # Don't publish invisible topics
     return unless @topic.visible?
 
diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb
index 1b45dd5d8..2c33d2350 100644
--- a/lib/site_setting_extension.rb
+++ b/lib/site_setting_extension.rb
@@ -34,6 +34,10 @@ module SiteSettingExtension
     @enums ||= {}
   end
 
+  def hidden_settings
+    @hidden_settings ||= []
+  end
+
   def setting(name, default = nil, opts = {})
     mutex.synchronize do
       self.defaults[name] = default
@@ -42,6 +46,9 @@ module SiteSettingExtension
         enum = opts[:enum]
         enums[name] = enum.is_a?(String) ? enum.constantize : enum
       end
+      if opts[:hidden] == true
+        hidden_settings << name
+      end
       setup_methods(name, current_value)
     end
   end
@@ -76,16 +83,18 @@ module SiteSettingExtension
   end
 
   # Retrieve all settings
-  def all_settings
-    @defaults.map do |s, v|
-      value = send(s)
-      type = types[get_data_type(s, value)]
-      {setting: s,
-       description: description(s),
-       default: v,
-       type: type.to_s,
-       value: value.to_s}.merge( type == :enum ? {valid_values: enum_class(s).values, translate_names: enum_class(s).translate_names?} : {})
-    end
+  def all_settings(include_hidden=false)
+    @defaults
+      .reject{|s, v| hidden_settings.include?(s) || include_hidden}
+      .map do |s, v|
+        value = send(s)
+        type = types[get_data_type(s, value)]
+        {setting: s,
+         description: description(s),
+         default: v,
+         type: type.to_s,
+         value: value.to_s}.merge( type == :enum ? {valid_values: enum_class(s).values, translate_names: enum_class(s).translate_names?} : {})
+      end
   end
 
   def description(setting)
diff --git a/lib/site_settings/local_process_provider.rb b/lib/site_settings/local_process_provider.rb
index 10532a6f4..1ab5e42c1 100644
--- a/lib/site_settings/local_process_provider.rb
+++ b/lib/site_settings/local_process_provider.rb
@@ -4,16 +4,20 @@ class SiteSettings::LocalProcessProvider
 
   Setting = Struct.new(:name, :value, :data_type) unless defined? SiteSettings::LocalProcessProvider::Setting
 
-  def initialize
+  def initialize(defaults = {})
     @settings = {}
+    @defaults = {}
+    defaults.each do |name,(value,data_type)|
+      @defaults[name] = Setting.new(name,value,data_type)
+    end
   end
 
   def all
-    @settings.values
+    (@defaults.merge @settings).values
   end
 
   def find(name)
-    @settings[name]
+    @settings[name] || @defaults[name]
   end
 
   def save(name, value, data_type)
diff --git a/lib/topic_query.rb b/lib/topic_query.rb
index c31dc24eb..b6b442218 100644
--- a/lib/topic_query.rb
+++ b/lib/topic_query.rb
@@ -50,7 +50,7 @@ class TopicQuery
     # If you've clearned the pin, use bumped_at, otherwise put it at the top
     def order_nocategory_with_pinned_sql
       "CASE
-        WHEN topics.category_id IS NULL and (COALESCE(topics.pinned_at, '#{lowest_date}') > COALESCE(tu.cleared_pinned_at, '#{lowest_date}'))
+        WHEN topics.category_id = #{SiteSetting.uncategorized_category_id.to_i} and (COALESCE(topics.pinned_at, '#{lowest_date}') > COALESCE(tu.cleared_pinned_at, '#{lowest_date}'))
           THEN '#{highest_date}'
         ELSE topics.bumped_at
        END DESC"
@@ -58,7 +58,7 @@ class TopicQuery
 
     # For anonymous users
     def order_nocategory_basic_bumped
-      "CASE WHEN topics.category_id IS NULL and (topics.pinned_at IS NOT NULL) THEN 0 ELSE 1 END, topics.bumped_at DESC"
+      "CASE WHEN topics.category_id = #{SiteSetting.uncategorized_category_id.to_i} and (topics.pinned_at IS NOT NULL) THEN 0 ELSE 1 END, topics.bumped_at DESC"
     end
 
     def order_basic_bumped
@@ -152,18 +152,6 @@ class TopicQuery
     TopicList.new(:private_messages, user, list)
   end
 
-  def list_uncategorized
-    create_list(:uncategorized, unordered: true) do |list|
-      list = list.where(category_id: nil)
-
-      if @user
-        list.order(TopicQuery.order_with_pinned_sql)
-      else
-        list.order(TopicQuery.order_nocategory_basic_bumped)
-      end
-    end
-  end
-
   def list_category(category)
     create_list(:category, unordered: true) do |list|
       list = list.where(category_id: category.id)
diff --git a/spec/components/category_list_spec.rb b/spec/components/category_list_spec.rb
index ef27f65e1..97f8f8d17 100644
--- a/spec/components/category_list_spec.rb
+++ b/spec/components/category_list_spec.rb
@@ -6,35 +6,6 @@ describe CategoryList do
   let(:user) { Fabricate(:user) }
   let(:category_list) { CategoryList.new(Guardian.new user) }
 
-  context "with no categories" do
-
-    it "has no categories" do
-      category_list.categories.should be_blank
-    end
-
-    context "with an uncategorized topic" do
-      let!(:topic) { Fabricate(:topic)}
-      let(:category) { category_list.categories.first }
-
-      it "has the right category" do
-        category.should be_present
-        category.name.should == SiteSetting.uncategorized_name
-        category.slug.should == SiteSetting.uncategorized_name
-        category.topics_week.should == 1
-        category.featured_topics.should == [topic]
-        category.displayable_topics.should == [topic] # CategoryDetailedSerializer needs this attribute
-      end
-
-      it 'does not return an invisible topic' do
-        invisible_topic = Fabricate(:topic)
-        invisible_topic.update_status('visible', false, Fabricate(:admin))
-        expect(category.featured_topics).to_not include(invisible_topic)
-      end
-
-    end
-
-  end
-
   context "security" do
     it "properly hide secure categories" do
       admin = Fabricate(:admin)
@@ -45,7 +16,9 @@ describe CategoryList do
       cat.set_permissions(:admins => :full)
       cat.save
 
-      CategoryList.new(Guardian.new admin).categories.count.should == 1
+      # uncategorized + this
+      CategoryList.new(Guardian.new admin).categories.count.should == 2
+
       CategoryList.new(Guardian.new user).categories.count.should == 0
       CategoryList.new(Guardian.new nil).categories.count.should == 0
     end
@@ -75,13 +48,12 @@ describe CategoryList do
       it 'returns the empty category and a non-empty category for those who can create them' do
         category_with_topics = Fabricate(:topic, category: Fabricate(:category))
         Guardian.any_instance.expects(:can_create?).with(Category).returns(true)
-        category_list.categories.should have(2).categories
+        category_list.categories.should have(3).categories
         category_list.categories.should include(topic_category)
       end
 
     end
 
-
     context "with a topic in a category" do
       let!(:topic) { Fabricate(:topic, category: topic_category)}
       let(:category) { category_list.categories.first }
diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb
index 065035569..e9e60c173 100644
--- a/spec/components/guardian_spec.rb
+++ b/spec/components/guardian_spec.rb
@@ -687,9 +687,6 @@ describe Guardian do
 
   end
 
-
-
-
   context 'can_delete?' do
 
     it 'returns false with a nil object' do
@@ -697,6 +694,10 @@ describe Guardian do
     end
 
     context 'a Topic' do
+      before do
+        # pretend we have a real topic
+        topic.id = 9999999
+      end
 
       it 'returns false when not logged in' do
         Guardian.new.can_delete?(topic).should be_false
diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb
index 1b44d360d..3cd82bef2 100644
--- a/spec/components/topic_query_spec.rb
+++ b/spec/components/topic_query_spec.rb
@@ -49,7 +49,7 @@ describe TopicQuery do
 
     context 'list_latest' do
       it "returns the topics in the correct order" do
-        topics.should == [pinned_topic, closed_topic, archived_topic, regular_topic]
+        topics.map(&:title).should == [pinned_topic, closed_topic, archived_topic, regular_topic].map(&:title)
       end
 
       it "includes the invisible topic if you're a moderator" do
@@ -80,10 +80,6 @@ describe TopicQuery do
     let!(:topic_no_cat) { Fabricate(:topic) }
     let!(:topic_in_cat) { Fabricate(:topic, category: category) }
 
-    it "returns the topic without a category when filtering uncategorized" do
-      topic_query.list_uncategorized.topics.should == [topic_no_cat]
-    end
-
     it "returns the topic with a category when filtering by category" do
       topic_query.list_category(category).topics.should == [topic_category, topic_in_cat]
     end
diff --git a/spec/components/trashable_spec.rb b/spec/components/trashable_spec.rb
index 7a0cca945..779816f76 100644
--- a/spec/components/trashable_spec.rb
+++ b/spec/components/trashable_spec.rb
@@ -7,12 +7,8 @@ describe Trashable do
     p1 = Fabricate(:post)
     p2 = Fabricate(:post)
 
-    Post.count.should == 2
-    p1.trash!
-
-    Post.count.should == 1
-
-    Post.with_deleted.count.should == 2
+    expect { p1.trash! }.to change{Post.count}.by(-1)
+    Post.with_deleted.count.should == Post.count + 1
   end
 end
 
diff --git a/spec/controllers/categories_controller_spec.rb b/spec/controllers/categories_controller_spec.rb
index 7ca122bef..fead81681 100644
--- a/spec/controllers/categories_controller_spec.rb
+++ b/spec/controllers/categories_controller_spec.rb
@@ -58,7 +58,7 @@ describe CategoriesController do
                               }
 
           response.status.should == 200
-          category = Category.first
+          category = Category.where(name: "hello").first
           category.category_groups.map{|g| [g.group_id, g.permission_type]}.sort.should == [
             [Group[:everyone].id, readonly],[Group[:staff].id,create_post]
           ]
diff --git a/spec/controllers/list_controller_spec.rb b/spec/controllers/list_controller_spec.rb
index 5254ba508..5994e0db8 100644
--- a/spec/controllers/list_controller_spec.rb
+++ b/spec/controllers/list_controller_spec.rb
@@ -27,7 +27,8 @@ describe ListController do
     end
 
     it 'allows users to filter on a set of topic ids' do
-      p = Fabricate(:post)
+      p = create_post
+
       xhr :get, :latest, format: :json, topic_ids: "#{p.topic_id}"
       response.should be_success
       parsed = JSON.parse(response.body)
@@ -128,38 +129,7 @@ describe ListController do
           response.content_type.should == 'application/rss+xml'
         end
       end
-
     end
-
-    context 'uncategorized' do
-
-      it "doesn't check access to see the category, since we didn't provide one" do
-        Guardian.any_instance.expects(:can_see?).never
-        xhr :get, :category, category: SiteSetting.uncategorized_name
-      end
-
-      it "responds with success" do
-        xhr :get, :category, category: SiteSetting.uncategorized_name
-        response.should be_success
-      end
-
-      context 'SiteSetting.uncategorized_name is non standard' do
-        before do
-          SiteSetting.stubs(:uncategorized_name).returns('testing')
-        end
-
-        it "responds with success given SiteSetting.uncategorized_name" do
-          xhr :get, :category, category: SiteSetting.uncategorized_name
-          response.should be_success
-        end
-
-        it 'responds with success given "uncategorized"' do
-          xhr :get, :category, category: 'uncategorized'
-          response.should be_success
-        end
-      end
-    end
-
   end
 
   describe "topics_by" do
diff --git a/spec/fabricators/topic_fabricator.rb b/spec/fabricators/topic_fabricator.rb
index 0a2e16ba1..c05fe83f0 100644
--- a/spec/fabricators/topic_fabricator.rb
+++ b/spec/fabricators/topic_fabricator.rb
@@ -1,6 +1,7 @@
 Fabricator(:topic) do
   user
   title { sequence(:title) { |i| "This is a test topic #{i}" } }
+  category_id { SiteSetting.uncategorized_category_id }
 end
 
 Fabricator(:deleted_topic, from: :topic) do
diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb
index a58eb8357..10867eab7 100644
--- a/spec/models/category_spec.rb
+++ b/spec/models/category_spec.rb
@@ -31,6 +31,9 @@ describe Category do
 
   describe "topic_create_allowed and post_create_allowed" do
     it "works" do
+
+      # NOTE we also have the uncategorized category ... hence the increased count
+
       default_category = Fabricate(:category)
       full_category = Fabricate(:category)
       can_post_category = Fabricate(:category)
@@ -54,20 +57,20 @@ describe Category do
       can_read_category.save
 
       guardian = Guardian.new(admin)
-      Category.topic_create_allowed(guardian).count.should == 4
-      Category.post_create_allowed(guardian).count.should == 4
-      Category.secured(guardian).count.should == 4
+      Category.topic_create_allowed(guardian).count.should == 5
+      Category.post_create_allowed(guardian).count.should == 5
+      Category.secured(guardian).count.should == 5
 
       guardian = Guardian.new(user)
-      Category.secured(guardian).count.should == 4
-      Category.post_create_allowed(guardian).count.should == 3
-      Category.topic_create_allowed(guardian).count.should == 2 # explicitly allowed once, default allowed once
+      Category.secured(guardian).count.should == 5
+      Category.post_create_allowed(guardian).count.should == 4
+      Category.topic_create_allowed(guardian).count.should == 3 # explicitly allowed once, default allowed once
 
       # everyone has special semantics, test it as well
       can_post_category.set_permissions(:everyone => :create_post)
       can_post_category.save
 
-      Category.post_create_allowed(guardian).count.should == 3
+      Category.post_create_allowed(guardian).count.should == 4
 
       # anonymous has permission to create no topics
       guardian = Guardian.new(nil)
@@ -105,22 +108,16 @@ describe Category do
     end
 
     it "lists all secured categories correctly" do
+      uncategorized = Category.first
+
       group.add(user)
       category.set_permissions(group.id => :full)
       category.save
       category_2.set_permissions(group.id => :full)
       category_2.save
 
-      Category.secured.should =~ []
-      Category.secured(Guardian.new(user)).should =~ [category, category_2]
-    end
-  end
-
-  describe "uncategorized name" do
-    let(:category) { Fabricate.build(:category, name: SiteSetting.uncategorized_name) }
-
-    it "is invalid to create a category with the reserved name" do
-      category.should_not be_valid
+      Category.secured.should =~ [uncategorized]
+      Category.secured(Guardian.new(user)).should =~ [uncategorized,category, category_2]
     end
   end
 
diff --git a/spec/models/post_mover_spec.rb b/spec/models/post_mover_spec.rb
index 605e2b24a..5ae21949d 100644
--- a/spec/models/post_mover_spec.rb
+++ b/spec/models/post_mover_spec.rb
@@ -113,7 +113,7 @@ describe PostMover do
           moved_to.highest_post_number.should == 3
           moved_to.featured_user1_id.should == another_user.id
           moved_to.like_count.should == 1
-          moved_to.category.should be_blank
+          moved_to.category_id.should == SiteSetting.uncategorized_category_id
 
           # Posts should be re-ordered
           p2.reload
diff --git a/spec/models/site_spec.rb b/spec/models/site_spec.rb
index 771400e74..63ed9a7d8 100644
--- a/spec/models/site_spec.rb
+++ b/spec/models/site_spec.rb
@@ -6,12 +6,16 @@ describe Site do
     category = Fabricate(:category)
     user = Fabricate(:user)
 
-    Site.new(Guardian.new(user)).categories.count.should == 1
+    Site.new(Guardian.new(user)).categories.count.should == 2
 
     category.set_permissions(:everyone => :create_post)
     category.save
 
-    # TODO clean up querying so we can make sure we have the correct permission set
-    Site.new(Guardian.new(user)).categories[0].permission.should_not == CategoryGroup.permission_types[:full]
+    Site.new(Guardian.new(user))
+        .categories
+        .keep_if{|c| c.name == category.name}
+        .first
+        .permission
+        .should_not == CategoryGroup.permission_types[:full]
   end
 end
diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb
index c2f6864a1..92fb51d5b 100644
--- a/spec/models/topic_spec.rb
+++ b/spec/models/topic_spec.rb
@@ -171,8 +171,6 @@ describe Topic do
         topic.fancy_title.should == "&ldquo;this topic&rdquo; &ndash; has &ldquo;fancy stuff&rdquo;"
       end
     end
-
-
   end
 
   context 'category validation' do
@@ -182,7 +180,7 @@ describe Topic do
       end
 
       it "does not allow nil category" do
-        topic = Fabricate(:topic, category: nil)
+        topic = Fabricate.build(:topic, category: nil)
         topic.should_not be_valid
         topic.errors[:category_id].should be_present
       end
@@ -796,7 +794,7 @@ describe Topic do
     describe 'without a previous category' do
 
       it 'should not change the topic_count when not changed' do
-       lambda { @topic.change_category(nil); @category.reload }.should_not change(@category, :topic_count)
+       lambda { @topic.change_category(@topic.category.name); @category.reload }.should_not change(@category, :topic_count)
       end
 
       describe 'changed category' do
@@ -812,10 +810,9 @@ describe Topic do
 
       end
 
-
       it "doesn't change the category when it can't be found" do
         @topic.change_category('made up')
-        @topic.category.should be_blank
+        @topic.category_id.should == SiteSetting.uncategorized_category_id
       end
     end
 
@@ -876,7 +873,7 @@ describe Topic do
         end
 
         it "resets the category" do
-          @topic.category_id.should be_blank
+          @topic.category_id.should == SiteSetting.uncategorized_category_id
           @category.topic_count.should == 0
         end
       end
@@ -955,7 +952,7 @@ describe Topic do
         it "ignores the category's default auto-close" do
           Timecop.freeze(Time.zone.now) do
             Jobs.expects(:enqueue_at).with(7.days.from_now, :close_topic, all_of( has_key(:topic_id), has_key(:user_id) ))
-            Fabricate(:topic, auto_close_days: 7, user: Fabricate(:admin), category: Fabricate(:category, auto_close_days: 2))
+            Fabricate(:topic, auto_close_days: 7, user: Fabricate(:admin), category_id: Fabricate(:category, auto_close_days: 2).id)
           end
         end
 
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index b658128f3..5d9db3851 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -77,8 +77,13 @@ Spork.prefork do
 
     config.before(:all) do
       DiscoursePluginRegistry.clear
+      uncat_id = SiteSetting.uncategorized_category_id
+
       Discourse.current_user_provider = TestCurrentUserProvider
 
+      # a bit odd, but this setting is actually preloaded
+      SiteSetting.defaults[:uncategorized_category_id] = SiteSetting.uncategorized_category_id
+
       require_dependency 'site_settings/local_process_provider'
       SiteSetting.provider = SiteSettings::LocalProcessProvider.new
     end