Refactor some trove admin bits, add some test coverage
diff --git a/Allura/allura/controllers/trovecategories.py b/Allura/allura/controllers/trovecategories.py
index 6edb9d4..91450fd 100644
--- a/Allura/allura/controllers/trovecategories.py
+++ b/Allura/allura/controllers/trovecategories.py
@@ -38,6 +38,15 @@
     add_category_form = forms.AddTroveCategoryForm()
 
 
+class TroveAdminException(Exception):
+    def __init__(self, flash_args, redir_params='', upper=None):
+        super(TroveAdminException, self).__init__()
+
+        self.flash_args = flash_args
+        self.redir_params = redir_params
+        self.upper = upper
+
+
 class TroveCategoryController(BaseController):
     @expose()
     def _lookup(self, trove_cat_id, *remainder):
@@ -102,29 +111,22 @@
         }
         return dict(tree=OrderedDict(sorted(tree.iteritems())))
 
-    @expose()
-    @require_post()
-    @validate(F.add_category_form, error_handler=index)
-    def create(self, **kw):
-        name = kw.get('categoryname')
-        upper_id = int(kw.get('uppercategory_id', 0))
-        shortname = kw.get('shortname', None)
+    @classmethod
+    def _create(cls, name, upper_id, shortname):
 
         upper = M.TroveCategory.query.get(trove_cat_id=upper_id)
         if upper_id == 0:
             path = name
             show_as_skill = True
         elif upper is None:
-            flash('Invalid upper category.', "error")
-            redirect('/categories')
-            return
+            raise TroveAdminException(('Invalid upper category.', "error"))
         else:
             path = upper.fullpath + " :: " + name
             show_as_skill = upper.show_as_skill
 
         newid = max(
             [el.trove_cat_id for el in M.TroveCategory.query.find()]) + 1
-        shortname = h.slugify(shortname or name)[1]
+        shortname = h.slugify(shortname or name, True)[1]
 
         if upper:
             trove_type = upper.fullpath.split(' :: ')[0]
@@ -133,10 +135,13 @@
             # no parent, so making a top-level.  Don't limit fullpath_re, so enforcing global uniqueness
             fullpath_re = re.compile(r'')
         oldcat = M.TroveCategory.query.get(shortname=shortname, fullpath=fullpath_re)
+
         if oldcat:
-            flash('A category with shortname "%s" already exists (%s).  Try a different, unique shortname'
-                  % (shortname, oldcat.fullpath), "error")
-            redir_params = u'?categoryname={}&shortname={}'.format(name, shortname)
+            raise TroveAdminException(
+                ('A category with shortname "%s" already exists (%s).  Try a different, unique shortname' % (shortname, oldcat.fullpath), "error"),
+                u'?categoryname={}&shortname={}'.format(name, shortname),
+                upper
+            )
         else:
             M.TroveCategory(
                 trove_cat_id=newid,
@@ -145,8 +150,25 @@
                 shortname=shortname,
                 fullpath=path,
                 show_as_skill=show_as_skill)
-            flash('Category "%s" successfully created.' % name)
-            redir_params = ''
+            return upper, ('Category "%s" successfully created.' % name,), ''
+
+    @expose()
+    @require_post()
+    @validate(F.add_category_form, error_handler=index)
+    def create(self, **kw):
+        name = kw.get('categoryname')
+        upper_id = int(kw.get('uppercategory_id', 0))
+        shortname = kw.get('shortname', None)
+
+        try:
+            upper, flash_args, redir_params = self._create(name, upper_id, shortname)
+        except TroveAdminException as ex:
+            upper = ex.upper
+            flash_args = ex.flash_args
+            redir_params = ex.redir_params
+
+        flash(*flash_args)
+
         if upper:
             redirect(u'/categories/{}/{}'.format(upper.trove_cat_id, redir_params))
         else:
diff --git a/Allura/allura/tests/functional/test_trovecategory.py b/Allura/allura/tests/functional/test_trovecategory.py
index 346a9f1..2dc438f 100644
--- a/Allura/allura/tests/functional/test_trovecategory.py
+++ b/Allura/allura/tests/functional/test_trovecategory.py
@@ -18,7 +18,7 @@
 import mock
 
 from tg import config
-from nose.tools import assert_equals, assert_true, assert_in
+from nose.tools import assert_equals, assert_true, assert_in, assert_equal
 from ming.orm import session
 
 from allura import model as M
@@ -149,4 +149,53 @@
         form = r.forms[0]
         r = form.submit()
         assert_in("Category removed", self.webflash(r))
-        assert_equals(4, M.TroveCategory.query.find().count())
\ No newline at end of file
+        assert_equals(4, M.TroveCategory.query.find().count())
+
+    def test_create_parent(self):
+        self.create_some_cats()
+        session(M.TroveCategory).flush()
+        r = self.app.get('/categories/')
+
+        form = r.forms[1]
+        form['categoryname'].value = "New Category"
+        form.submit()
+
+        possible = M.TroveCategory.query.find(dict(fullname='New Category')).all()
+        assert_equal(len(possible), 1)
+        assert_equal(possible[0].fullname, 'New Category')
+        assert_equal(possible[0].shortname, 'new-category')
+
+    def test_create_child(self):
+        self.create_some_cats()
+        session(M.TroveCategory).flush()
+        r = self.app.get('/categories/2')
+
+        form = r.forms[2]
+        form['categoryname'].value = "New Child"
+        form.submit()
+
+        possible =M.TroveCategory.query.find(dict(fullname='New Child')).all()
+        assert_equal(len(possible), 1)
+        assert_equal(possible[0].fullname, 'New Child')
+        assert_equal(possible[0].shortname, 'new-child')
+        assert_equal(possible[0].trove_parent_id, 2)
+
+        # test slugify with periods. the relevant form becomes the third, after a child has been created above.
+        r = self.app.get('/categories/2')
+        form = r.forms[3]
+        form['categoryname'].value = "New Child.io"
+        form.submit()
+        possible = M.TroveCategory.query.find(dict(fullname='New Child.io')).all()
+        assert_equal(possible[0].shortname, 'new-child.io')
+
+    def test_create_child_bad_upper(self):
+        self.create_some_cats()
+        session(M.TroveCategory).flush()
+        r = self.app.get('/categories/2')
+
+        form = r.forms[2]
+        form['categoryname'].value = "New Child"
+        form['uppercategory_id'].value = "541561615"
+        r = form.submit().follow()
+
+        assert 'Invalid upper category' in r.text