diff --git a/rhodecode/api/__init__.py b/rhodecode/api/__init__.py
--- a/rhodecode/api/__init__.py
+++ b/rhodecode/api/__init__.py
@@ -30,7 +30,8 @@ from pyramid.renderers import render
from pyramid.response import Response
from pyramid.httpexceptions import HTTPNotFound
-from rhodecode.api.exc import JSONRPCBaseError, JSONRPCError, JSONRPCForbidden
+from rhodecode.api.exc import (
+ JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
from rhodecode.lib.auth import AuthUser
from rhodecode.lib.base import get_ip_addr
from rhodecode.lib.ext_json import json
@@ -127,6 +128,11 @@ def exception_view(exc, request):
if isinstance(exc, JSONRPCError):
fault_message = exc.message
log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
+ elif isinstance(exc, JSONRPCValidationError):
+ colander_exc = exc.colander_exception
+ #TODO: think maybe of nicer way to serialize errors ?
+ fault_message = colander_exc.asdict()
+ log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
elif isinstance(exc, JSONRPCForbidden):
fault_message = 'Access was denied to this resource.'
log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
diff --git a/rhodecode/api/exc.py b/rhodecode/api/exc.py
--- a/rhodecode/api/exc.py
+++ b/rhodecode/api/exc.py
@@ -27,5 +27,13 @@ class JSONRPCError(JSONRPCBaseError):
pass
+class JSONRPCValidationError(JSONRPCBaseError):
+
+ def __init__(self, *args, **kwargs):
+ self.colander_exception = kwargs.pop('colander_exc')
+ super(JSONRPCValidationError, self).__init__(*args, **kwargs)
+
+
class JSONRPCForbidden(JSONRPCBaseError):
pass
+
diff --git a/rhodecode/api/tests/test_create_gist.py b/rhodecode/api/tests/test_create_gist.py
--- a/rhodecode/api/tests/test_create_gist.py
+++ b/rhodecode/api/tests/test_create_gist.py
@@ -43,7 +43,7 @@ class TestApiCreateGist(object):
description='foobar-gist',
gist_type=gist_type,
acl_level=gist_acl_level,
- files={'foobar': {'content': 'foo'}})
+ files={'foobar_ąć': {'content': 'foo'}})
response = api_call(self.app, params)
response_json = response.json
gist = response_json['result']['gist']
@@ -68,6 +68,32 @@ class TestApiCreateGist(object):
finally:
Fixture().destroy_gists()
+ @pytest.mark.parametrize("expected, lifetime, gist_type, gist_acl_level, files", [
+ ({'gist_type': '"ups" is not one of private, public'},
+ 10, 'ups', Gist.ACL_LEVEL_PUBLIC, {'f': {'content': 'f'}}),
+
+ ({'lifetime': '-120 is less than minimum value -1'},
+ -120, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PUBLIC, {'f': {'content': 'f'}}),
+
+ ({'0.content': 'Required'},
+ 10, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PUBLIC, {'f': {'x': 'f'}}),
+ ])
+ def test_api_try_create_gist(
+ self, expected, lifetime, gist_type, gist_acl_level, files):
+ id_, params = build_data(
+ self.apikey_regular, 'create_gist',
+ lifetime=lifetime,
+ description='foobar-gist',
+ gist_type=gist_type,
+ acl_level=gist_acl_level,
+ files=files)
+ response = api_call(self.app, params)
+
+ try:
+ assert_error(id_, expected, given=response.body)
+ finally:
+ Fixture().destroy_gists()
+
@mock.patch.object(GistModel, 'create', crash)
def test_api_create_gist_exception_occurred(self):
id_, params = build_data(self.apikey_regular, 'create_gist', files={})
diff --git a/rhodecode/api/utils.py b/rhodecode/api/utils.py
--- a/rhodecode/api/utils.py
+++ b/rhodecode/api/utils.py
@@ -34,8 +34,6 @@ from rhodecode.lib.vcs.exceptions import
log = logging.getLogger(__name__)
-
-
class OAttr(object):
"""
Special Option that defines other attribute, and can default to them
diff --git a/rhodecode/api/views/gist_api.py b/rhodecode/api/views/gist_api.py
--- a/rhodecode/api/views/gist_api.py
+++ b/rhodecode/api/views/gist_api.py
@@ -23,6 +23,7 @@ import logging
import time
from rhodecode.api import jsonrpc_method, JSONRPCError
+from rhodecode.api.exc import JSONRPCValidationError
from rhodecode.api.utils import (
Optional, OAttr, get_gist_or_error, get_user_or_error,
has_superadmin_permission)
@@ -96,7 +97,8 @@ def get_gists(request, apiuser, userid=O
@jsonrpc_method()
def create_gist(
- request, apiuser, files, owner=Optional(OAttr('apiuser')),
+ request, apiuser, files, gistid=Optional(None),
+ owner=Optional(OAttr('apiuser')),
gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
acl_level=Optional(Gist.ACL_LEVEL_PUBLIC),
description=Optional('')):
@@ -108,10 +110,11 @@ def create_gist(
:param files: files to be added to the gist. The data structure has
to match the following example::
- {'filename': {'content':'...', 'lexer': null},
- 'filename2': {'content':'...', 'lexer': null}}
+ {'filename1': {'content':'...'}, 'filename2': {'content':'...'}}
:type files: dict
+ :param gistid: Set a custom id for the gist
+ :type gistid: Optional(str)
:param owner: Set the gist owner, defaults to api method caller
:type owner: Optional(str or int)
:param gist_type: type of gist ``public`` or ``private``
@@ -148,23 +151,49 @@ def create_gist(
}
"""
+ from rhodecode.model import validation_schema
+ from rhodecode.model.validation_schema.schemas import gist_schema
+
+ if isinstance(owner, Optional):
+ owner = apiuser.user_id
+
+ owner = get_user_or_error(owner)
+
+ lifetime = Optional.extract(lifetime)
+ schema = gist_schema.GistSchema().bind(
+ # bind the given values if it's allowed, however the deferred
+ # validator will still validate it according to other rules
+ lifetime_options=[lifetime])
try:
- if isinstance(owner, Optional):
- owner = apiuser.user_id
+ nodes = gist_schema.nodes_to_sequence(
+ files, colander_node=schema.get('nodes'))
+
+ schema_data = schema.deserialize(dict(
+ gistid=Optional.extract(gistid),
+ description=Optional.extract(description),
+ gist_type=Optional.extract(gist_type),
+ lifetime=lifetime,
+ gist_acl_level=Optional.extract(acl_level),
+ nodes=nodes
+ ))
- owner = get_user_or_error(owner)
- description = Optional.extract(description)
- gist_type = Optional.extract(gist_type)
- lifetime = Optional.extract(lifetime)
- acl_level = Optional.extract(acl_level)
+ # convert to safer format with just KEYs so we sure no duplicates
+ schema_data['nodes'] = gist_schema.sequence_to_nodes(
+ schema_data['nodes'], colander_node=schema.get('nodes'))
+
+ except validation_schema.Invalid as err:
+ raise JSONRPCValidationError(colander_exc=err)
- gist = GistModel().create(description=description,
- owner=owner,
- gist_mapping=files,
- gist_type=gist_type,
- lifetime=lifetime,
- gist_acl_level=acl_level)
+ try:
+ gist = GistModel().create(
+ owner=owner,
+ gist_id=schema_data['gistid'],
+ description=schema_data['description'],
+ gist_mapping=schema_data['nodes'],
+ gist_type=schema_data['gist_type'],
+ lifetime=schema_data['lifetime'],
+ gist_acl_level=schema_data['gist_acl_level'])
Session().commit()
return {
'msg': 'created new gist',
diff --git a/rhodecode/api/views/repo_api.py b/rhodecode/api/views/repo_api.py
--- a/rhodecode/api/views/repo_api.py
+++ b/rhodecode/api/views/repo_api.py
@@ -44,7 +44,7 @@ from rhodecode.model.repo import RepoMod
from rhodecode.model.repo_group import RepoGroupModel
from rhodecode.model.scm import ScmModel, RepoList
from rhodecode.model.settings import SettingsModel, VcsSettingsModel
-from rhodecode.model.validation_schema import RepoSchema
+from rhodecode.model.validation_schema.schemas import repo_schema
log = logging.getLogger(__name__)
@@ -610,7 +610,7 @@ def create_repo(request, apiuser, repo_n
}
"""
- schema = RepoSchema()
+ schema = repo_schema.RepoSchema()
try:
data = schema.deserialize({
'repo_name': repo_name
diff --git a/rhodecode/api/views/repo_group_api.py b/rhodecode/api/views/repo_group_api.py
--- a/rhodecode/api/views/repo_group_api.py
+++ b/rhodecode/api/views/repo_group_api.py
@@ -34,7 +34,7 @@ from rhodecode.lib.auth import (
from rhodecode.model.db import Session, RepoGroup
from rhodecode.model.repo_group import RepoGroupModel
from rhodecode.model.scm import RepoGroupList
-from rhodecode.model.validation_schema import RepoGroupSchema
+from rhodecode.model.validation_schema.schemas import repo_group_schema
log = logging.getLogger(__name__)
@@ -193,7 +193,7 @@ def create_repo_group(request, apiuser,
"""
- schema = RepoGroupSchema()
+ schema = repo_group_schema.RepoGroupSchema()
try:
data = schema.deserialize({
'group_name': group_name
diff --git a/rhodecode/controllers/admin/gists.py b/rhodecode/controllers/admin/gists.py
--- a/rhodecode/controllers/admin/gists.py
+++ b/rhodecode/controllers/admin/gists.py
@@ -25,15 +25,18 @@ gist controller for RhodeCode
import time
import logging
-import traceback
+
import formencode
+import peppercorn
from formencode import htmlfill
from pylons import request, response, tmpl_context as c, url
from pylons.controllers.util import abort, redirect
from pylons.i18n.translation import _
+from webob.exc import HTTPNotFound, HTTPForbidden
+from sqlalchemy.sql.expression import or_
-from rhodecode.model.forms import GistForm
+
from rhodecode.model.gist import GistModel
from rhodecode.model.meta import Session
from rhodecode.model.db import Gist, User
@@ -44,9 +47,10 @@ from rhodecode.lib.auth import LoginRequ
from rhodecode.lib.utils import jsonify
from rhodecode.lib.utils2 import safe_str, safe_int, time_to_datetime
from rhodecode.lib.ext_json import json
-from webob.exc import HTTPNotFound, HTTPForbidden
-from sqlalchemy.sql.expression import or_
from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
+from rhodecode.model import validation_schema
+from rhodecode.model.validation_schema.schemas import gist_schema
+
log = logging.getLogger(__name__)
@@ -56,11 +60,11 @@ class GistsController(BaseController):
def __load_defaults(self, extra_values=None):
c.lifetime_values = [
- (str(-1), _('forever')),
- (str(5), _('5 minutes')),
- (str(60), _('1 hour')),
- (str(60 * 24), _('1 day')),
- (str(60 * 24 * 30), _('1 month')),
+ (-1, _('forever')),
+ (5, _('5 minutes')),
+ (60, _('1 hour')),
+ (60 * 24, _('1 day')),
+ (60 * 24 * 30, _('1 month')),
]
if extra_values:
c.lifetime_values.append(extra_values)
@@ -136,40 +140,56 @@ class GistsController(BaseController):
"""POST /admin/gists: Create a new item"""
# url('gists')
self.__load_defaults()
- gist_form = GistForm([x[0] for x in c.lifetime_values],
- [x[0] for x in c.acl_options])()
+
+ data = dict(request.POST)
+ data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
+ data['nodes'] = [{
+ 'filename': data['filename'],
+ 'content': data.get('content'),
+ 'mimetype': data.get('mimetype') # None is autodetect
+ }]
+
+ data['gist_type'] = (
+ Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
+ data['gist_acl_level'] = (
+ data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
+
+ schema = gist_schema.GistSchema().bind(
+ lifetime_options=[x[0] for x in c.lifetime_values])
+
try:
- form_result = gist_form.to_python(dict(request.POST))
- # TODO: multiple files support, from the form
- filename = form_result['filename'] or Gist.DEFAULT_FILENAME
- nodes = {
- filename: {
- 'content': form_result['content'],
- 'lexer': form_result['mimetype'] # None is autodetect
- }
- }
- _public = form_result['public']
- gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
- gist_acl_level = form_result.get(
- 'acl_level', Gist.ACL_LEVEL_PRIVATE)
+
+ schema_data = schema.deserialize(data)
+ # convert to safer format with just KEYs so we sure no duplicates
+ schema_data['nodes'] = gist_schema.sequence_to_nodes(
+ schema_data['nodes'])
+
gist = GistModel().create(
- description=form_result['description'],
+ gist_id=schema_data['gistid'], # custom access id not real ID
+ description=schema_data['description'],
owner=c.rhodecode_user.user_id,
- gist_mapping=nodes,
- gist_type=gist_type,
- lifetime=form_result['lifetime'],
- gist_id=form_result['gistid'],
- gist_acl_level=gist_acl_level
+ gist_mapping=schema_data['nodes'],
+ gist_type=schema_data['gist_type'],
+ lifetime=schema_data['lifetime'],
+ gist_acl_level=schema_data['gist_acl_level']
)
Session().commit()
new_gist_id = gist.gist_access_id
- except formencode.Invalid as errors:
- defaults = errors.value
+ except validation_schema.Invalid as errors:
+ defaults = data
+ errors = errors.asdict()
+
+ if 'nodes.0.content' in errors:
+ errors['content'] = errors['nodes.0.content']
+ del errors['nodes.0.content']
+ if 'nodes.0.filename' in errors:
+ errors['filename'] = errors['nodes.0.filename']
+ del errors['nodes.0.filename']
return formencode.htmlfill.render(
render('admin/gists/new.html'),
defaults=defaults,
- errors=errors.error_dict or {},
+ errors=errors,
prefix_error=False,
encoding="UTF-8",
force_defaults=False
@@ -243,7 +263,8 @@ class GistsController(BaseController):
log.exception("Exception in gist show")
raise HTTPNotFound()
if format == 'raw':
- content = '\n\n'.join([f.content for f in c.files if (f_path is None or f.path == f_path)])
+ content = '\n\n'.join([f.content for f in c.files
+ if (f_path is None or f.path == f_path)])
response.content_type = 'text/plain'
return content
return render('admin/gists/show.html')
@@ -252,32 +273,35 @@ class GistsController(BaseController):
@NotAnonymous()
@auth.CSRFRequired()
def edit(self, gist_id):
+ self.__load_defaults()
self._add_gist_to_context(gist_id)
owner = c.gist.gist_owner == c.rhodecode_user.user_id
if not (h.HasPermissionAny('hg.admin')() or owner):
raise HTTPForbidden()
- rpost = request.POST
- nodes = {}
- _file_data = zip(rpost.getall('org_files'), rpost.getall('files'),
- rpost.getall('mimetypes'), rpost.getall('contents'))
- for org_filename, filename, mimetype, content in _file_data:
- nodes[org_filename] = {
- 'org_filename': org_filename,
- 'filename': filename,
- 'content': content,
- 'lexer': mimetype,
- }
+ data = peppercorn.parse(request.POST.items())
+
+ schema = gist_schema.GistSchema()
+ schema = schema.bind(
+ # '0' is special value to leave lifetime untouched
+ lifetime_options=[x[0] for x in c.lifetime_values] + [0],
+ )
+
try:
+ schema_data = schema.deserialize(data)
+ # convert to safer format with just KEYs so we sure no duplicates
+ schema_data['nodes'] = gist_schema.sequence_to_nodes(
+ schema_data['nodes'])
+
GistModel().update(
gist=c.gist,
- description=rpost['description'],
+ description=schema_data['description'],
owner=c.gist.owner,
- gist_mapping=nodes,
- gist_type=c.gist.gist_type,
- lifetime=rpost['lifetime'],
- gist_acl_level=rpost['acl_level']
+ gist_mapping=schema_data['nodes'],
+ gist_type=schema_data['gist_type'],
+ lifetime=schema_data['lifetime'],
+ gist_acl_level=schema_data['gist_acl_level']
)
Session().commit()
@@ -287,6 +311,10 @@ class GistsController(BaseController):
# store only DB stuff for gist
Session().commit()
h.flash(_('Successfully updated gist data'), category='success')
+ except validation_schema.Invalid as errors:
+ errors = errors.asdict()
+ h.flash(_('Error occurred during update of gist {}: {}').format(
+ gist_id, errors), category='error')
except Exception:
log.exception("Exception in gist edit")
h.flash(_('Error occurred during update of gist %s') % gist_id,
@@ -317,7 +345,7 @@ class GistsController(BaseController):
# this cannot use timeago, since it's used in select2 as a value
expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
self.__load_defaults(
- extra_values=('0', _('%(expiry)s - current value') % {'expiry': expiry}))
+ extra_values=(0, _('%(expiry)s - current value') % {'expiry': expiry}))
return render('admin/gists/edit.html')
@LoginRequired()
diff --git a/rhodecode/controllers/search.py b/rhodecode/controllers/search.py
--- a/rhodecode/controllers/search.py
+++ b/rhodecode/controllers/search.py
@@ -35,6 +35,7 @@ from rhodecode.lib.helpers import Page
from rhodecode.lib.utils2 import safe_str, safe_int
from rhodecode.lib.index import searcher_from_config
from rhodecode.model import validation_schema
+from rhodecode.model.validation_schema.schemas import search_schema
log = logging.getLogger(__name__)
@@ -48,7 +49,7 @@ class SearchController(BaseRepoControlle
formatted_results = []
execution_time = ''
- schema = validation_schema.SearchParamsSchema()
+ schema = search_schema.SearchParamsSchema()
search_params = {}
errors = []
@@ -75,7 +76,6 @@ class SearchController(BaseRepoControlle
page_limit = search_params['page_limit']
requested_page = search_params['requested_page']
-
c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
ip_addr=self.ip_addr)
diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py
--- a/rhodecode/model/forms.py
+++ b/rhodecode/model/forms.py
@@ -555,23 +555,6 @@ def PullRequestForm(repo_id):
return _PullRequestForm
-def GistForm(lifetime_options, acl_level_options):
- class _GistForm(formencode.Schema):
-
- gistid = All(v.UniqGistId(), v.UnicodeString(strip=True, min=3, not_empty=False, if_missing=None))
- filename = All(v.BasePath()(),
- v.UnicodeString(strip=True, required=False))
- description = v.UnicodeString(required=False, if_missing=u'')
- lifetime = v.OneOf(lifetime_options)
- mimetype = v.UnicodeString(required=False, if_missing=None)
- content = v.UnicodeString(required=True, not_empty=True)
- public = v.UnicodeString(required=False, if_missing=u'')
- private = v.UnicodeString(required=False, if_missing=u'')
- acl_level = v.OneOf(acl_level_options)
-
- return _GistForm
-
-
def IssueTrackerPatternsForm():
class _IssueTrackerPatternsForm(formencode.Schema):
allow_extra_fields = True
diff --git a/rhodecode/model/gist.py b/rhodecode/model/gist.py
--- a/rhodecode/model/gist.py
+++ b/rhodecode/model/gist.py
@@ -107,7 +107,7 @@ class GistModel(BaseModel):
:param description: description of the gist
:param owner: user who created this gist
- :param gist_mapping: mapping {filename:{'content':content},...}
+ :param gist_mapping: mapping [{'filename': 'file1.txt', 'content': content}, ...}]
:param gist_type: type of gist private/public
:param lifetime: in minutes, -1 == forever
:param gist_acl_level: acl level for this gist
@@ -141,25 +141,10 @@ class GistModel(BaseModel):
repo_name=gist_id, repo_type='hg', repo_group=GIST_STORE_LOC,
use_global_config=True)
- processed_mapping = {}
- for filename in gist_mapping:
- if filename != os.path.basename(filename):
- raise Exception('Filename cannot be inside a directory')
-
- content = gist_mapping[filename]['content']
- # TODO: expand support for setting explicit lexers
-# if lexer is None:
-# try:
-# guess_lexer = pygments.lexers.guess_lexer_for_filename
-# lexer = guess_lexer(filename,content)
-# except pygments.util.ClassNotFound:
-# lexer = 'text'
- processed_mapping[filename] = {'content': content}
-
# now create single multifile commit
message = 'added file'
- message += 's: ' if len(processed_mapping) > 1 else ': '
- message += ', '.join([x for x in processed_mapping])
+ message += 's: ' if len(gist_mapping) > 1 else ': '
+ message += ', '.join([x for x in gist_mapping])
# fake RhodeCode Repository object
fake_repo = AttributeDict({
@@ -170,7 +155,7 @@ class GistModel(BaseModel):
ScmModel().create_nodes(
user=owner.user_id, repo=fake_repo,
message=message,
- nodes=processed_mapping,
+ nodes=gist_mapping,
trigger_push_hook=False
)
@@ -196,7 +181,6 @@ class GistModel(BaseModel):
gist = self._get_gist(gist)
gist_repo = gist.scm_instance()
- lifetime = safe_int(lifetime, -1)
if lifetime == 0: # preserve old value
gist_expires = gist.gist_expires
else:
@@ -207,9 +191,9 @@ class GistModel(BaseModel):
gist_mapping_op = {}
for k, v in gist_mapping.items():
# add, mod, del
- if not v['org_filename'] and v['filename']:
+ if not v['filename_org'] and v['filename']:
op = 'add'
- elif v['org_filename'] and not v['filename']:
+ elif v['filename_org'] and not v['filename']:
op = 'del'
else:
op = 'mod'
diff --git a/rhodecode/model/validation_schema.py b/rhodecode/model/validation_schema.py
deleted file mode 100644
--- a/rhodecode/model/validation_schema.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2016-2016 RhodeCode GmbH
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License, version 3
-# (only), as published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-# This program is dual-licensed. If you wish to learn more about the
-# RhodeCode Enterprise Edition, including its added features, Support services,
-# and proprietary license terms, please see https://rhodecode.com/licenses/
-
-import colander
-from colander import Invalid # noqa
-
-
-class GroupNameType(colander.String):
- SEPARATOR = '/'
-
- def deserialize(self, node, cstruct):
- result = super(GroupNameType, self).deserialize(node, cstruct)
- return self._replace_extra_slashes(result)
-
- def _replace_extra_slashes(self, path):
- path = path.split(self.SEPARATOR)
- path = [item for item in path if item]
- return self.SEPARATOR.join(path)
-
-
-class RepoGroupSchema(colander.Schema):
- group_name = colander.SchemaNode(GroupNameType())
-
-
-class RepoSchema(colander.Schema):
- repo_name = colander.SchemaNode(GroupNameType())
-
-
-class SearchParamsSchema(colander.MappingSchema):
- search_query = colander.SchemaNode(
- colander.String(),
- missing='')
- search_type = colander.SchemaNode(
- colander.String(),
- missing='content',
- validator=colander.OneOf(['content', 'path', 'commit', 'repository']))
- search_sort = colander.SchemaNode(
- colander.String(),
- missing='newfirst',
- validator=colander.OneOf(
- ['oldfirst', 'newfirst']))
- page_limit = colander.SchemaNode(
- colander.Integer(),
- missing=10,
- validator=colander.Range(1, 500))
- requested_page = colander.SchemaNode(
- colander.Integer(),
- missing=1)
-
diff --git a/rhodecode/model/validation_schema/__init__.py b/rhodecode/model/validation_schema/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/model/validation_schema/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2016 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+import colander
+
+from colander import Invalid # noqa, don't remove this
+
diff --git a/rhodecode/model/validation_schema/preparers.py b/rhodecode/model/validation_schema/preparers.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/model/validation_schema/preparers.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2016 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+import unicodedata
+
+
+
+def strip_preparer(value):
+ """
+ strips given values using .strip() function
+ """
+
+ if value:
+ value = value.strip()
+ return value
+
+
+def slugify_preparer(value):
+ """
+ Slugify given value to a safe representation for url/id
+ """
+ from rhodecode.lib.utils import repo_name_slug
+ if value:
+ value = repo_name_slug(value.lower())
+ return value
+
+
+def non_ascii_strip_preparer(value):
+ """
+ trie to replace non-ascii letters to their ascii representation
+ eg::
+
+ `żołw` converts into `zolw`
+ """
+ if value:
+ value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
+ return value
+
+
+def unique_list_preparer(value):
+ """
+ Converts an list to a list with only unique values
+ """
+
+ def make_unique(value):
+ seen = []
+ return [c for c in value if
+ not (c in seen or seen.append(c))]
+
+ if isinstance(value, list):
+ ret_val = make_unique(value)
+ elif isinstance(value, set):
+ ret_val = list(value)
+ elif isinstance(value, tuple):
+ ret_val = make_unique(value)
+ elif value is None:
+ ret_val = []
+ else:
+ ret_val = [value]
+
+ return ret_val
+
+
+def unique_list_from_str_preparer(value):
+ """
+ Converts an list to a list with only unique values
+ """
+ from rhodecode.lib.utils2 import aslist
+
+ if isinstance(value, basestring):
+ value = aslist(value, ',')
+ return unique_list_preparer(value)
\ No newline at end of file
diff --git a/rhodecode/model/validation_schema/schemas/__init__.py b/rhodecode/model/validation_schema/schemas/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/model/validation_schema/schemas/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2016 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+"""
+Colander Schema nodes
+http://docs.pylonsproject.org/projects/colander/en/latest/basics.html#schema-node-objects
+"""
+
diff --git a/rhodecode/model/validation_schema/schemas/gist_schema.py b/rhodecode/model/validation_schema/schemas/gist_schema.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/model/validation_schema/schemas/gist_schema.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2016 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+import os
+
+import colander
+
+from rhodecode.translation import _
+from rhodecode.model.validation_schema import validators, preparers
+
+
+def nodes_to_sequence(nodes, colander_node=None):
+ """
+ Converts old style dict nodes to new list of dicts
+
+ :param nodes: dict with key beeing name of the file
+
+ """
+ if not isinstance(nodes, dict):
+ msg = 'Nodes needs to be a dict, got {}'.format(type(nodes))
+ raise colander.Invalid(colander_node, msg)
+ out = []
+
+ for key, val in nodes.items():
+ val = (isinstance(val, dict) and val) or {}
+ out.append(dict(
+ filename=key,
+ content=val.get('content'),
+ mimetype=val.get('mimetype')
+ ))
+
+ out = Nodes().deserialize(out)
+ return out
+
+
+def sequence_to_nodes(nodes, colander_node=None):
+ if not isinstance(nodes, list):
+ msg = 'Nodes needs to be a list, got {}'.format(type(nodes))
+ raise colander.Invalid(colander_node, msg)
+ nodes = Nodes().deserialize(nodes)
+
+ out = {}
+ try:
+ for file_data in nodes:
+ file_data_skip = file_data.copy()
+ # if we got filename_org we use it as a key so we keep old
+ # name as input and rename is-reflected inside the values as
+ # filename and filename_org differences.
+ filename_org = file_data.get('filename_org')
+ filename = filename_org or file_data['filename']
+ out[filename] = {}
+ out[filename].update(file_data_skip)
+
+ except Exception as e:
+ msg = 'Invalid data format org_exc:`{}`'.format(repr(e))
+ raise colander.Invalid(colander_node, msg)
+ return out
+
+
+@colander.deferred
+def deferred_lifetime_validator(node, kw):
+ options = kw.get('lifetime_options', [])
+ return colander.All(
+ colander.Range(min=-1, max=60 * 24 * 30 * 12),
+ colander.OneOf([x for x in options]))
+
+
+def unique_gist_validator(node, value):
+ from rhodecode.model.db import Gist
+ existing = Gist.get_by_access_id(value)
+ if existing:
+ msg = _(u'Gist with name {} already exists').format(value)
+ raise colander.Invalid(node, msg)
+
+
+def filename_validator(node, value):
+ if value != os.path.basename(value):
+ msg = _(u'Filename {} cannot be inside a directory').format(value)
+ raise colander.Invalid(node, msg)
+
+
+class NodeSchema(colander.MappingSchema):
+ # if we perform rename this will be org filename
+ filename_org = colander.SchemaNode(
+ colander.String(),
+ preparer=[preparers.strip_preparer,
+ preparers.non_ascii_strip_preparer],
+ validator=filename_validator,
+ missing=None)
+
+ filename = colander.SchemaNode(
+ colander.String(),
+ preparer=[preparers.strip_preparer,
+ preparers.non_ascii_strip_preparer],
+ validator=filename_validator)
+
+ content = colander.SchemaNode(
+ colander.String())
+ mimetype = colander.SchemaNode(
+ colander.String(),
+ missing=None)
+
+
+class Nodes(colander.SequenceSchema):
+ filenames = NodeSchema()
+
+ def validator(self, node, cstruct):
+ if not isinstance(cstruct, list):
+ return
+
+ found_filenames = []
+ for data in cstruct:
+ filename = data['filename']
+ if filename in found_filenames:
+ msg = _('Duplicated value for filename found: `{}`').format(
+ filename)
+ raise colander.Invalid(node, msg)
+ found_filenames.append(filename)
+
+
+class GistSchema(colander.MappingSchema):
+ """
+ schema = GistSchema()
+ schema.bind(
+ lifetime_options = [1,2,3]
+ )
+ out = schema.deserialize(dict(
+ nodes=[
+ {'filename': 'x', 'content': 'xxx', },
+ {'filename': 'docs/Z', 'content': 'xxx', 'mimetype': 'x'},
+ ]
+ ))
+ """
+
+ from rhodecode.model.db import Gist
+
+ gistid = colander.SchemaNode(
+ colander.String(),
+ missing=None,
+ preparer=[preparers.strip_preparer,
+ preparers.non_ascii_strip_preparer,
+ preparers.slugify_preparer],
+ validator=colander.All(
+ colander.Length(min=3),
+ unique_gist_validator
+ ))
+
+ description = colander.SchemaNode(
+ colander.String(),
+ missing=u'')
+
+ lifetime = colander.SchemaNode(
+ colander.Integer(),
+ validator=deferred_lifetime_validator)
+
+ gist_acl_level = colander.SchemaNode(
+ colander.String(),
+ validator=colander.OneOf([Gist.ACL_LEVEL_PUBLIC,
+ Gist.ACL_LEVEL_PRIVATE]))
+
+ gist_type = colander.SchemaNode(
+ colander.String(),
+ missing=Gist.ACL_LEVEL_PUBLIC,
+ validator=colander.OneOf([Gist.GIST_PRIVATE, Gist.GIST_PUBLIC]))
+
+ nodes = Nodes()
+
+
diff --git a/rhodecode/model/validation_schema/schemas/repo_group_schema.py b/rhodecode/model/validation_schema/schemas/repo_group_schema.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/model/validation_schema/schemas/repo_group_schema.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2016 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+
+import colander
+
+
+from rhodecode.model.validation_schema import validators, preparers, types
+
+
+class RepoGroupSchema(colander.Schema):
+ group_name = colander.SchemaNode(types.GroupNameType())
diff --git a/rhodecode/model/validation_schema/schemas/repo_schema.py b/rhodecode/model/validation_schema/schemas/repo_schema.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/model/validation_schema/schemas/repo_schema.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2016 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+import colander
+
+from rhodecode.model.validation_schema import validators, preparers, types
+
+
+class RepoSchema(colander.Schema):
+ repo_name = colander.SchemaNode(types.GroupNameType())
diff --git a/rhodecode/model/validation_schema/schemas/search_schema.py b/rhodecode/model/validation_schema/schemas/search_schema.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/model/validation_schema/schemas/search_schema.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2016 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+
+import colander
+
+
+class SearchParamsSchema(colander.MappingSchema):
+ search_query = colander.SchemaNode(
+ colander.String(),
+ missing='')
+ search_type = colander.SchemaNode(
+ colander.String(),
+ missing='content',
+ validator=colander.OneOf(['content', 'path', 'commit', 'repository']))
+ search_sort = colander.SchemaNode(
+ colander.String(),
+ missing='newfirst',
+ validator=colander.OneOf(
+ ['oldfirst', 'newfirst']))
+ page_limit = colander.SchemaNode(
+ colander.Integer(),
+ missing=10,
+ validator=colander.Range(1, 500))
+ requested_page = colander.SchemaNode(
+ colander.Integer(),
+ missing=1)
diff --git a/rhodecode/model/validation_schema/types.py b/rhodecode/model/validation_schema/types.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/model/validation_schema/types.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2016-2016 RhodeCode GmbH
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License, version 3
+# (only), as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# This program is dual-licensed. If you wish to learn more about the
+# RhodeCode Enterprise Edition, including its added features, Support services,
+# and proprietary license terms, please see https://rhodecode.com/licenses/
+
+import colander
+
+
+class GroupNameType(colander.String):
+ SEPARATOR = '/'
+
+ def deserialize(self, node, cstruct):
+ result = super(GroupNameType, self).deserialize(node, cstruct)
+ return self._replace_extra_slashes(result)
+
+ def _replace_extra_slashes(self, path):
+ path = path.split(self.SEPARATOR)
+ path = [item for item in path if item]
+ return self.SEPARATOR.join(path)
diff --git a/rhodecode/model/validation_schema/validators.py b/rhodecode/model/validation_schema/validators.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/model/validation_schema/validators.py
@@ -0,0 +1,19 @@
+import os
+
+import ipaddress
+import colander
+
+from rhodecode.translation import _
+
+
+def ip_addr_validator(node, value):
+ try:
+ # this raises an ValueError if address is not IpV4 or IpV6
+ ipaddress.ip_network(value, strict=False)
+ except ValueError:
+ msg = _(u'Please enter a valid IPv4 or IpV6 address')
+ raise colander.Invalid(node, msg)
+
+
+
+
diff --git a/rhodecode/model/validators.py b/rhodecode/model/validators.py
--- a/rhodecode/model/validators.py
+++ b/rhodecode/model/validators.py
@@ -970,22 +970,6 @@ def FieldKey():
return _validator
-def BasePath():
- class _validator(formencode.validators.FancyValidator):
- messages = {
- 'badPath': _(u'Filename cannot be inside a directory'),
- }
-
- def _to_python(self, value, state):
- return value
-
- def validate_python(self, value, state):
- if value != os.path.basename(value):
- raise formencode.Invalid(self.message('badPath', state),
- value, state)
- return _validator
-
-
def ValidAuthPlugins():
class _validator(formencode.validators.FancyValidator):
messages = {
@@ -1061,26 +1045,6 @@ def ValidAuthPlugins():
return _validator
-def UniqGistId():
- class _validator(formencode.validators.FancyValidator):
- messages = {
- 'gistid_taken': _(u'This gistid is already in use')
- }
-
- def _to_python(self, value, state):
- return repo_name_slug(value.lower())
-
- def validate_python(self, value, state):
- existing = Gist.get_by_access_id(value)
- if existing:
- msg = M(self, 'gistid_taken', state)
- raise formencode.Invalid(
- msg, value, state, error_dict={'gistid': msg}
- )
-
- return _validator
-
-
def ValidPattern():
class _Validator(formencode.validators.FancyValidator):
diff --git a/rhodecode/templates/admin/gists/edit.html b/rhodecode/templates/admin/gists/edit.html
--- a/rhodecode/templates/admin/gists/edit.html
+++ b/rhodecode/templates/admin/gists/edit.html
@@ -44,27 +44,31 @@
${h.dropdownmenu('lifetime', '0', c.lifetime_options)}
-
- ${h.dropdownmenu('acl_level', c.gist.acl_level, c.acl_options)}
+
+ ${h.dropdownmenu('gist_acl_level', c.gist.acl_level, c.acl_options)}
+ ## peppercorn schema
+
% for cnt, file in enumerate(c.files):
+