[#8380] generate unique shortnames, remove confusing ProjectName(Type) holder of name+shortname
diff --git a/Allura/allura/controllers/rest.py b/Allura/allura/controllers/rest.py
index 51b6798..24ee77d 100644
--- a/Allura/allura/controllers/rest.py
+++ b/Allura/allura/controllers/rest.py
@@ -394,9 +394,9 @@
         jsondata = json.loads(request.body)
         projectSchema = make_newproject_schema(self._neighborhood)
         try:
-            pdata = deserialize_project(jsondata, projectSchema)
-            shortname = pdata.shortname or pdata.name.shortname
-            project_reg.validate_project(self._neighborhood, shortname, pdata.name.name, c.user,
+            pdata = deserialize_project(jsondata, projectSchema, self._neighborhood)
+            shortname = pdata.shortname
+            project_reg.validate_project(self._neighborhood, shortname, pdata.name, c.user,
                                          user_project=False, private_project=pdata.private)
         except (colander.Invalid, ForgeError) as e:
             response.status_int = 400
diff --git a/Allura/allura/lib/project_create_helpers.py b/Allura/allura/lib/project_create_helpers.py
index 94a62e8..2796995 100644
--- a/Allura/allura/lib/project_create_helpers.py
+++ b/Allura/allura/lib/project_create_helpers.py
@@ -17,7 +17,6 @@
 
 from __future__ import unicode_literals
 from __future__ import absolute_import
-import re
 from io import BytesIO
 
 import datetime
@@ -27,9 +26,11 @@
 import colander as col
 import bson
 import requests
+import formencode
 import six
 from six.moves.urllib.parse import urlparse
 
+from allura.lib.helpers import slugify
 from allura.model import Neighborhood
 from ming.base import Object
 from ming.orm import ThreadLocalORMSession
@@ -81,24 +82,6 @@
         return user
 
 
-class ProjectName(object):
-
-    def __init__(self, name, shortname):
-        self.name = name
-        self.shortname = shortname
-
-
-class ProjectNameType():
-
-    def deserialize(self, node, cstruct):
-        if cstruct is col.null:
-            return col.null
-        name = cstruct
-        shortname = re.sub("[^A-Za-z0-9 ]", "", name).lower()
-        shortname = re.sub(" ", "-", shortname)
-        return ProjectName(name, shortname)
-
-
 class ProjectShortnameType():
 
     def __init__(self, nbhd, update):
@@ -177,7 +160,7 @@
     def schema_type(self, **kw):
         return col.Mapping(unknown='raise')
 
-    name = col.SchemaNode(ProjectNameType())
+    name = col.SchemaNode(col.Str())
     summary = col.SchemaNode(col.Str(), missing='')
     description = col.SchemaNode(col.Str(), missing='')
     admin = col.SchemaNode(User())
@@ -207,8 +190,8 @@
 
 def make_newproject_schema(nbhd, update=False):
     # type: (Neighborhood, bool) -> NewProjectSchema
-    # dynamically add to the schema (e.g. if needs nbhd)
     projectSchema = NewProjectSchema(unknown='raise')
+    # dynamically add to the schema fields that depend on `nbhd`
     projectSchema.add(col.SchemaNode(col.Sequence(),
                                      col.SchemaNode(Award(nbhd)),
                                      name='awards', missing=[]))
@@ -217,32 +200,62 @@
     return projectSchema
 
 
-def deserialize_project(datum, projectSchema):
-    # type: (dict, NewProjectSchema) -> object
+def deserialize_project(datum, projectSchema, nbhd):
+    # type: (dict, NewProjectSchema, Neighborhood) -> object
     p = projectSchema.deserialize(datum)
     p = Object(p)  # convert from dict to something with attr-access
+
+    # generate a shortname, and try to make it unique
+    if not p.shortname:
+        max_shortname_len = 15  # maybe more depending on NeighborhoodProjectShortNameValidator impl, but this is safe
+        shortname = orig_shortname = make_shortname(p.name, max_shortname_len)
+        for i in range(1, 10):
+            try:
+                ProjectRegistrationProvider.get().shortname_validator.to_python(shortname, neighborhood=nbhd)
+            except formencode.api.Invalid:
+                if len(orig_shortname) == max_shortname_len - 1:
+                    shortname = orig_shortname + str(i)
+                else:
+                    shortname = orig_shortname[:max_shortname_len - 1] + str(i)
+            else:
+                # we're good!
+                break
+        p.shortname = shortname
+
     return p
 
 
+def make_shortname(name, max_len):
+    # lowercase, drop periods and underscores
+    shortname = slugify(name)[1].replace('_', '-')
+    # must start with a letter
+    if not shortname[0].isalpha():
+        shortname = 'a-' + shortname
+    # truncate length, avoid trailing dash
+    shortname = shortname[:max_len].rstrip('-')
+    # too short
+    if len(shortname) < 3:
+        shortname += '-z'
+    return shortname
+
+
 def create_project_with_attrs(p, nbhd, update=False, ensure_tools=False):
     # type: (object, M.Neighborhood, bool, bool) -> Union[M.Project|bool]
     M.session.artifact_orm_session._get().skip_mod_date = True
-    shortname = p.shortname or p.name.shortname
+    shortname = p.shortname
     project = M.Project.query.get(shortname=shortname,
                                   neighborhood_id=nbhd._id)
     project_template = nbhd.get_project_template()
 
-    if project and not (update and p.shortname):
-        log.warning('Skipping existing project "%s". To update an existing '
-                    'project you must provide the project shortname and run '
-                    'this script with --update.' % (shortname))
+    if project and not update:
+        log.warning('Skipping existing project "%s"' % (shortname))
         return False
 
     if not project:
         creating = True
         project = nbhd.register_project(shortname,
                                         p.admin,
-                                        project_name=p.name.name,
+                                        project_name=p.name,
                                         private_project=p.private)
     else:
         creating = False
diff --git a/Allura/allura/tests/functional/test_rest.py b/Allura/allura/tests/functional/test_rest.py
index c66686b..56fed67 100644
--- a/Allura/allura/tests/functional/test_rest.py
+++ b/Allura/allura/tests/functional/test_rest.py
@@ -537,6 +537,22 @@
         assert_equal(p.get_tool_data('allura', 'grouping_threshold'), 5)
         assert_equal(p.admins()[0].username, 'test-admin')
 
+    def test_add_project_automatic_shortname(self):
+        # no shortname given, and name "Test" would conflict with existing "test" project
+        project_data = {
+            "admin": "test-admin",
+            "name": "Test",
+        }
+        r = self.api_post('/rest/p/add_project',
+                          params=json.dumps(project_data),
+                          user='root',
+                          status=201)
+        assert_equal(r.json, {
+            'status': 'success',
+            'html_url': 'http://localhost/p/test1/',
+            'url': 'http://localhost/rest/p/test1/',
+        })
+
 
 class TestDoap(TestRestApiBase):
     validate_skip = True
diff --git a/scripts/project-import.py b/scripts/project-import.py
index 0cba5db..fc09a78 100644
--- a/scripts/project-import.py
+++ b/scripts/project-import.py
@@ -50,7 +50,9 @@
     projects = []
     for datum in data:
         try:
-            projects.append(deserialize_project(datum, projectSchema))
+            if options.update and not datum.get('shortname'):
+                log.warning('Shortname not provided with --update; this will create new projects instead of updating')
+            projects.append(deserialize_project(datum, projectSchema, nbhd))
         except Exception:
             keep_going = options.validate_only
             log.error('Error on %s\n%s', datum['shortname'], datum, exc_info=keep_going)
@@ -63,8 +65,7 @@
         return
 
     for p in projects:
-        shortname = p.shortname or p.name.shortname
-        log.info('Creating%s project "%s".' % ('/updating' if options.update else '', shortname))
+        log.info('Creating%s project "%s".' % ('/updating' if options.update else '', p.shortname))
         try:
             project = create_project_with_attrs(p, nbhd, update=options.update, ensure_tools=options.ensure_tools)
         except Exception as e: