[#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: