Merge "Validate Project and Project Groups names"
This commit is contained in:
@@ -23,6 +23,7 @@ import wsmeext.pecan as wsme_pecan
|
||||
import storyboard.api.auth.authorization_checks as checks
|
||||
from storyboard.api.v1 import base
|
||||
from storyboard.api.v1.projects import Project
|
||||
from storyboard.common.custom_types import NameType
|
||||
from storyboard.db.api import project_groups
|
||||
|
||||
|
||||
@@ -32,9 +33,10 @@ CONF = cfg.CONF
|
||||
class ProjectGroup(base.APIBase):
|
||||
"""Represents a group of projects."""
|
||||
|
||||
name = wtypes.text
|
||||
"""A unique name, used in URLs, identifying the project group. All
|
||||
lowercase, no special characters. Examples: infra, compute.
|
||||
name = NameType()
|
||||
"""The Project Group unique name. This name will be displayed in the URL.
|
||||
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
|
||||
separators.
|
||||
"""
|
||||
|
||||
title = wtypes.text
|
||||
|
||||
@@ -25,6 +25,7 @@ import wsmeext.pecan as wsme_pecan
|
||||
|
||||
from storyboard.api.auth import authorization_checks as checks
|
||||
from storyboard.api.v1 import base
|
||||
from storyboard.common.custom_types import NameType
|
||||
from storyboard.db.api import projects as projects_api
|
||||
|
||||
CONF = cfg.CONF
|
||||
@@ -37,9 +38,10 @@ class Project(base.APIBase):
|
||||
Storyboard as Projects, among others.
|
||||
"""
|
||||
|
||||
name = wtypes.text
|
||||
"""At least one lowercase letter or number, followed by letters, numbers,
|
||||
dots, hyphens or pluses. Keep this name short; it is used in URLs.
|
||||
name = NameType()
|
||||
"""The Project unique name. This name will be displayed in the URL.
|
||||
At least 3 alphanumeric symbols. Minus and dot symbols are allowed as
|
||||
separators.
|
||||
"""
|
||||
|
||||
description = wtypes.text
|
||||
|
||||
31
storyboard/common/custom_types.py
Normal file
31
storyboard/common/custom_types.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright (c) 2014 Mirantis Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from wsme import types
|
||||
|
||||
|
||||
class NameType(types.StringType):
|
||||
"""This type should be applied to the name fields. Currently this type
|
||||
should be applied to Projects and Project Groups.
|
||||
|
||||
This type allows alphanumeric characters with . - and / separators inside
|
||||
the name. The name should be at least 3 symbols long.
|
||||
|
||||
"""
|
||||
|
||||
_name_regex = r'^[a-zA-Z0-9]+([\-\./]?[a-zA-Z0-9]+)*$'
|
||||
|
||||
def __init__(self):
|
||||
super(NameType, self).__init__(min_length=3, pattern=self._name_regex)
|
||||
@@ -0,0 +1,80 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""Existing projects should be renamed if they do not pass a new validation.
|
||||
The previous name will be appended to the description so that it can be
|
||||
restored.
|
||||
|
||||
Revision ID: 020
|
||||
Revises: 019
|
||||
Create Date: 2014-06-23 12:50:43.924601
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '020'
|
||||
down_revision = '019'
|
||||
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import MetaData
|
||||
from sqlalchemy.sql.expression import table
|
||||
|
||||
from storyboard.common.custom_types import NameType
|
||||
|
||||
|
||||
def upgrade(active_plugins=None, options=None):
|
||||
|
||||
bind = op.get_bind()
|
||||
validator = NameType()
|
||||
|
||||
projects = list(bind.execute(
|
||||
sa.select(columns=['*'], from_obj=sa.Table('projects', MetaData()))))
|
||||
|
||||
projects_table = table(
|
||||
'projects',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('description', sa.UnicodeText(), nullable=True),
|
||||
)
|
||||
|
||||
last_idx = 0
|
||||
|
||||
for project in projects:
|
||||
project_name = project["name"]
|
||||
project_id = project["id"]
|
||||
need_rename = False
|
||||
|
||||
try:
|
||||
validator.validate(project_name)
|
||||
except Exception:
|
||||
need_rename = True
|
||||
|
||||
if need_rename:
|
||||
# This project needs renaming
|
||||
temp_name = "Project-%d" % last_idx
|
||||
last_idx += 1
|
||||
updated_description = "%s This project was renamed to fit new " \
|
||||
"naming validation. Original name was: %s" \
|
||||
% (project["description"], project_name)
|
||||
|
||||
bind.execute(projects_table.update()
|
||||
.where(projects_table.c.id == project_id)
|
||||
.values(name=temp_name,
|
||||
description=updated_description))
|
||||
|
||||
|
||||
def downgrade(active_plugins=None, options=None):
|
||||
# No way back for invalid names
|
||||
pass
|
||||
@@ -13,6 +13,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import six
|
||||
import warnings
|
||||
import yaml
|
||||
@@ -21,12 +22,14 @@ from oslo.config import cfg
|
||||
from sqlalchemy.exc import SADeprecationWarning
|
||||
from storyboard.db.api import base as db_api
|
||||
|
||||
from storyboard.common.custom_types import NameType
|
||||
from storyboard.db.models import Project
|
||||
from storyboard.db.models import ProjectGroup
|
||||
|
||||
|
||||
warnings.simplefilter("ignore", SADeprecationWarning)
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def do_load_models(filename):
|
||||
@@ -34,20 +37,34 @@ def do_load_models(filename):
|
||||
config_file = open(filename)
|
||||
projects_list = yaml.load(config_file)
|
||||
|
||||
validator = NameType()
|
||||
|
||||
project_groups = dict()
|
||||
|
||||
for project in projects_list:
|
||||
|
||||
if not project.get('use-storyboard'):
|
||||
continue
|
||||
|
||||
group_name = project.get("group") or "default"
|
||||
if group_name not in project_groups:
|
||||
project_groups[group_name] = list()
|
||||
|
||||
project_name = project.get("project")
|
||||
|
||||
try:
|
||||
validator.validate(project_name)
|
||||
except Exception:
|
||||
# Skipping invalid project names
|
||||
LOG.warn("Project %s was not loaded. Validation failed."
|
||||
% project_name)
|
||||
continue
|
||||
|
||||
project_description = project.get("description")
|
||||
|
||||
project_groups[group_name].append({"name": project_name,
|
||||
"description": project_description})
|
||||
project_groups[group_name].append(
|
||||
{"name": project_name,
|
||||
"description": project_description})
|
||||
|
||||
session = db_api.get_session()
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
import json
|
||||
|
||||
from webtest.app import AppError
|
||||
|
||||
from storyboard.tests import base
|
||||
|
||||
|
||||
@@ -25,7 +27,7 @@ class TestProjects(base.FunctionalTest):
|
||||
self.resource = '/projects'
|
||||
|
||||
self.project_01 = {
|
||||
'name': 'test_project',
|
||||
'name': 'test-project',
|
||||
'description': 'some description'
|
||||
}
|
||||
|
||||
@@ -42,13 +44,21 @@ class TestProjects(base.FunctionalTest):
|
||||
self.assertEqual(self.project_01['description'],
|
||||
project['description'])
|
||||
|
||||
def test_create_invalid(self):
|
||||
|
||||
invalid_project = self.project_01.copy()
|
||||
invalid_project["name"] = "name with spaces"
|
||||
|
||||
self.assertRaises(AppError, self.post_json, self.resource,
|
||||
invalid_project)
|
||||
|
||||
def test_update(self):
|
||||
response = self.post_json(self.resource, self.project_01)
|
||||
original = json.loads(response.body)
|
||||
|
||||
delta = {
|
||||
'id': original['id'],
|
||||
'name': 'new name',
|
||||
'name': 'new-name',
|
||||
'description': 'new description'
|
||||
}
|
||||
|
||||
@@ -62,3 +72,22 @@ class TestProjects(base.FunctionalTest):
|
||||
self.assertNotEqual(original['name'], updated['name'])
|
||||
self.assertNotEqual(original['description'],
|
||||
updated['description'])
|
||||
|
||||
def test_update_invalid(self):
|
||||
response = self.post_json(self.resource, self.project_01)
|
||||
original = json.loads(response.body)
|
||||
|
||||
delta = {
|
||||
'id': original['id'],
|
||||
'name': 'new-name is invalid!',
|
||||
}
|
||||
|
||||
url = "/projects/%d" % original['id']
|
||||
|
||||
# check for invalid characters like space and '!'
|
||||
self.assertRaises(AppError, self.put_json, url, delta)
|
||||
|
||||
delta["name"] = "a"
|
||||
|
||||
# check for a too short name
|
||||
self.assertRaises(AppError, self.put_json, url, delta)
|
||||
|
||||
Reference in New Issue
Block a user