##// END OF EJS Templates
gists: use colander schema to validate input data....
marcink -
r523:878882bd default
parent child Browse files
Show More
@@ -0,0 +1,24 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22
23 from colander import Invalid # noqa, don't remove this
24
@@ -0,0 +1,89 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import unicodedata
22
23
24
25 def strip_preparer(value):
26 """
27 strips given values using .strip() function
28 """
29
30 if value:
31 value = value.strip()
32 return value
33
34
35 def slugify_preparer(value):
36 """
37 Slugify given value to a safe representation for url/id
38 """
39 from rhodecode.lib.utils import repo_name_slug
40 if value:
41 value = repo_name_slug(value.lower())
42 return value
43
44
45 def non_ascii_strip_preparer(value):
46 """
47 trie to replace non-ascii letters to their ascii representation
48 eg::
49
50 `żołw` converts into `zolw`
51 """
52 if value:
53 value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
54 return value
55
56
57 def unique_list_preparer(value):
58 """
59 Converts an list to a list with only unique values
60 """
61
62 def make_unique(value):
63 seen = []
64 return [c for c in value if
65 not (c in seen or seen.append(c))]
66
67 if isinstance(value, list):
68 ret_val = make_unique(value)
69 elif isinstance(value, set):
70 ret_val = list(value)
71 elif isinstance(value, tuple):
72 ret_val = make_unique(value)
73 elif value is None:
74 ret_val = []
75 else:
76 ret_val = [value]
77
78 return ret_val
79
80
81 def unique_list_from_str_preparer(value):
82 """
83 Converts an list to a list with only unique values
84 """
85 from rhodecode.lib.utils2 import aslist
86
87 if isinstance(value, basestring):
88 value = aslist(value, ',')
89 return unique_list_preparer(value) No newline at end of file
@@ -0,0 +1,25 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 """
22 Colander Schema nodes
23 http://docs.pylonsproject.org/projects/colander/en/latest/basics.html#schema-node-objects
24 """
25
@@ -0,0 +1,185 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22
23 import colander
24
25 from rhodecode.translation import _
26 from rhodecode.model.validation_schema import validators, preparers
27
28
29 def nodes_to_sequence(nodes, colander_node=None):
30 """
31 Converts old style dict nodes to new list of dicts
32
33 :param nodes: dict with key beeing name of the file
34
35 """
36 if not isinstance(nodes, dict):
37 msg = 'Nodes needs to be a dict, got {}'.format(type(nodes))
38 raise colander.Invalid(colander_node, msg)
39 out = []
40
41 for key, val in nodes.items():
42 val = (isinstance(val, dict) and val) or {}
43 out.append(dict(
44 filename=key,
45 content=val.get('content'),
46 mimetype=val.get('mimetype')
47 ))
48
49 out = Nodes().deserialize(out)
50 return out
51
52
53 def sequence_to_nodes(nodes, colander_node=None):
54 if not isinstance(nodes, list):
55 msg = 'Nodes needs to be a list, got {}'.format(type(nodes))
56 raise colander.Invalid(colander_node, msg)
57 nodes = Nodes().deserialize(nodes)
58
59 out = {}
60 try:
61 for file_data in nodes:
62 file_data_skip = file_data.copy()
63 # if we got filename_org we use it as a key so we keep old
64 # name as input and rename is-reflected inside the values as
65 # filename and filename_org differences.
66 filename_org = file_data.get('filename_org')
67 filename = filename_org or file_data['filename']
68 out[filename] = {}
69 out[filename].update(file_data_skip)
70
71 except Exception as e:
72 msg = 'Invalid data format org_exc:`{}`'.format(repr(e))
73 raise colander.Invalid(colander_node, msg)
74 return out
75
76
77 @colander.deferred
78 def deferred_lifetime_validator(node, kw):
79 options = kw.get('lifetime_options', [])
80 return colander.All(
81 colander.Range(min=-1, max=60 * 24 * 30 * 12),
82 colander.OneOf([x for x in options]))
83
84
85 def unique_gist_validator(node, value):
86 from rhodecode.model.db import Gist
87 existing = Gist.get_by_access_id(value)
88 if existing:
89 msg = _(u'Gist with name {} already exists').format(value)
90 raise colander.Invalid(node, msg)
91
92
93 def filename_validator(node, value):
94 if value != os.path.basename(value):
95 msg = _(u'Filename {} cannot be inside a directory').format(value)
96 raise colander.Invalid(node, msg)
97
98
99 class NodeSchema(colander.MappingSchema):
100 # if we perform rename this will be org filename
101 filename_org = colander.SchemaNode(
102 colander.String(),
103 preparer=[preparers.strip_preparer,
104 preparers.non_ascii_strip_preparer],
105 validator=filename_validator,
106 missing=None)
107
108 filename = colander.SchemaNode(
109 colander.String(),
110 preparer=[preparers.strip_preparer,
111 preparers.non_ascii_strip_preparer],
112 validator=filename_validator)
113
114 content = colander.SchemaNode(
115 colander.String())
116 mimetype = colander.SchemaNode(
117 colander.String(),
118 missing=None)
119
120
121 class Nodes(colander.SequenceSchema):
122 filenames = NodeSchema()
123
124 def validator(self, node, cstruct):
125 if not isinstance(cstruct, list):
126 return
127
128 found_filenames = []
129 for data in cstruct:
130 filename = data['filename']
131 if filename in found_filenames:
132 msg = _('Duplicated value for filename found: `{}`').format(
133 filename)
134 raise colander.Invalid(node, msg)
135 found_filenames.append(filename)
136
137
138 class GistSchema(colander.MappingSchema):
139 """
140 schema = GistSchema()
141 schema.bind(
142 lifetime_options = [1,2,3]
143 )
144 out = schema.deserialize(dict(
145 nodes=[
146 {'filename': 'x', 'content': 'xxx', },
147 {'filename': 'docs/Z', 'content': 'xxx', 'mimetype': 'x'},
148 ]
149 ))
150 """
151
152 from rhodecode.model.db import Gist
153
154 gistid = colander.SchemaNode(
155 colander.String(),
156 missing=None,
157 preparer=[preparers.strip_preparer,
158 preparers.non_ascii_strip_preparer,
159 preparers.slugify_preparer],
160 validator=colander.All(
161 colander.Length(min=3),
162 unique_gist_validator
163 ))
164
165 description = colander.SchemaNode(
166 colander.String(),
167 missing=u'')
168
169 lifetime = colander.SchemaNode(
170 colander.Integer(),
171 validator=deferred_lifetime_validator)
172
173 gist_acl_level = colander.SchemaNode(
174 colander.String(),
175 validator=colander.OneOf([Gist.ACL_LEVEL_PUBLIC,
176 Gist.ACL_LEVEL_PRIVATE]))
177
178 gist_type = colander.SchemaNode(
179 colander.String(),
180 missing=Gist.ACL_LEVEL_PUBLIC,
181 validator=colander.OneOf([Gist.GIST_PRIVATE, Gist.GIST_PUBLIC]))
182
183 nodes = Nodes()
184
185
@@ -0,0 +1,29 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import colander
23
24
25 from rhodecode.model.validation_schema import validators, preparers, types
26
27
28 class RepoGroupSchema(colander.Schema):
29 group_name = colander.SchemaNode(types.GroupNameType())
@@ -0,0 +1,27 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22
23 from rhodecode.model.validation_schema import validators, preparers, types
24
25
26 class RepoSchema(colander.Schema):
27 repo_name = colander.SchemaNode(types.GroupNameType())
@@ -0,0 +1,44 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 import colander
23
24
25 class SearchParamsSchema(colander.MappingSchema):
26 search_query = colander.SchemaNode(
27 colander.String(),
28 missing='')
29 search_type = colander.SchemaNode(
30 colander.String(),
31 missing='content',
32 validator=colander.OneOf(['content', 'path', 'commit', 'repository']))
33 search_sort = colander.SchemaNode(
34 colander.String(),
35 missing='newfirst',
36 validator=colander.OneOf(
37 ['oldfirst', 'newfirst']))
38 page_limit = colander.SchemaNode(
39 colander.Integer(),
40 missing=10,
41 validator=colander.Range(1, 500))
42 requested_page = colander.SchemaNode(
43 colander.Integer(),
44 missing=1)
@@ -0,0 +1,34 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22
23
24 class GroupNameType(colander.String):
25 SEPARATOR = '/'
26
27 def deserialize(self, node, cstruct):
28 result = super(GroupNameType, self).deserialize(node, cstruct)
29 return self._replace_extra_slashes(result)
30
31 def _replace_extra_slashes(self, path):
32 path = path.split(self.SEPARATOR)
33 path = [item for item in path if item]
34 return self.SEPARATOR.join(path)
@@ -0,0 +1,19 b''
1 import os
2
3 import ipaddress
4 import colander
5
6 from rhodecode.translation import _
7
8
9 def ip_addr_validator(node, value):
10 try:
11 # this raises an ValueError if address is not IpV4 or IpV6
12 ipaddress.ip_network(value, strict=False)
13 except ValueError:
14 msg = _(u'Please enter a valid IPv4 or IpV6 address')
15 raise colander.Invalid(node, msg)
16
17
18
19
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,100 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22 import pytest
23
24 from rhodecode.model import validation_schema
25 from rhodecode.model.validation_schema.schemas import gist_schema
26
27
28 class TestGistSchema(object):
29
30 def test_deserialize_bad_data(self):
31 schema = gist_schema.GistSchema().bind(
32 lifetime_options=[1, 2, 3]
33 )
34 with pytest.raises(validation_schema.Invalid) as exc_info:
35 schema.deserialize('err')
36 err = exc_info.value.asdict()
37 assert err[''] == '"err" is not a mapping type: ' \
38 'Does not implement dict-like functionality.'
39
40 def test_deserialize_bad_lifetime_options(self):
41 schema = gist_schema.GistSchema().bind(
42 lifetime_options=[1, 2, 3]
43 )
44 with pytest.raises(validation_schema.Invalid) as exc_info:
45 schema.deserialize(dict(
46 lifetime=10
47 ))
48 err = exc_info.value.asdict()
49 assert err['lifetime'] == '"10" is not one of 1, 2, 3'
50
51 with pytest.raises(validation_schema.Invalid) as exc_info:
52 schema.deserialize(dict(
53 lifetime='x'
54 ))
55 err = exc_info.value.asdict()
56 assert err['lifetime'] == '"x" is not a number'
57
58 def test_serialize_data_correctly(self):
59 schema = gist_schema.GistSchema().bind(
60 lifetime_options=[1, 2, 3]
61 )
62 nodes = [{
63 'filename': 'foobar',
64 'filename_org': 'foobar',
65 'content': 'content',
66 'mimetype': 'xx'
67 }]
68 schema_data = schema.deserialize(dict(
69 lifetime=2,
70 gist_type='public',
71 gist_acl_level='acl_public',
72 nodes=nodes,
73 ))
74
75 assert schema_data['nodes'] == nodes
76
77 def test_serialize_data_correctly_with_conversion(self):
78 schema = gist_schema.GistSchema().bind(
79 lifetime_options=[1, 2, 3],
80 convert_nodes=True
81 )
82 nodes = [{
83 'filename': 'foobar',
84 'filename_org': None,
85 'content': 'content',
86 'mimetype': 'xx'
87 }]
88 schema_data = schema.deserialize(dict(
89 lifetime=2,
90 gist_type='public',
91 gist_acl_level='acl_public',
92 nodes=nodes,
93 ))
94
95 assert schema_data['nodes'] == nodes
96
97 seq_nodes = gist_schema.sequence_to_nodes(nodes)
98 assert isinstance(seq_nodes, dict)
99 seq_nodes = gist_schema.nodes_to_sequence(seq_nodes)
100 assert nodes == seq_nodes
@@ -30,7 +30,8 b' from pyramid.renderers import render'
30 from pyramid.response import Response
30 from pyramid.response import Response
31 from pyramid.httpexceptions import HTTPNotFound
31 from pyramid.httpexceptions import HTTPNotFound
32
32
33 from rhodecode.api.exc import JSONRPCBaseError, JSONRPCError, JSONRPCForbidden
33 from rhodecode.api.exc import (
34 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
34 from rhodecode.lib.auth import AuthUser
35 from rhodecode.lib.auth import AuthUser
35 from rhodecode.lib.base import get_ip_addr
36 from rhodecode.lib.base import get_ip_addr
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.ext_json import json
@@ -127,6 +128,11 b' def exception_view(exc, request):'
127 if isinstance(exc, JSONRPCError):
128 if isinstance(exc, JSONRPCError):
128 fault_message = exc.message
129 fault_message = exc.message
129 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
130 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
131 elif isinstance(exc, JSONRPCValidationError):
132 colander_exc = exc.colander_exception
133 #TODO: think maybe of nicer way to serialize errors ?
134 fault_message = colander_exc.asdict()
135 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
130 elif isinstance(exc, JSONRPCForbidden):
136 elif isinstance(exc, JSONRPCForbidden):
131 fault_message = 'Access was denied to this resource.'
137 fault_message = 'Access was denied to this resource.'
132 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
138 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
@@ -27,5 +27,13 b' class JSONRPCError(JSONRPCBaseError):'
27 pass
27 pass
28
28
29
29
30 class JSONRPCValidationError(JSONRPCBaseError):
31
32 def __init__(self, *args, **kwargs):
33 self.colander_exception = kwargs.pop('colander_exc')
34 super(JSONRPCValidationError, self).__init__(*args, **kwargs)
35
36
30 class JSONRPCForbidden(JSONRPCBaseError):
37 class JSONRPCForbidden(JSONRPCBaseError):
31 pass
38 pass
39
@@ -43,7 +43,7 b' class TestApiCreateGist(object):'
43 description='foobar-gist',
43 description='foobar-gist',
44 gist_type=gist_type,
44 gist_type=gist_type,
45 acl_level=gist_acl_level,
45 acl_level=gist_acl_level,
46 files={'foobar': {'content': 'foo'}})
46 files={'foobar_ąć': {'content': 'foo'}})
47 response = api_call(self.app, params)
47 response = api_call(self.app, params)
48 response_json = response.json
48 response_json = response.json
49 gist = response_json['result']['gist']
49 gist = response_json['result']['gist']
@@ -68,6 +68,32 b' class TestApiCreateGist(object):'
68 finally:
68 finally:
69 Fixture().destroy_gists()
69 Fixture().destroy_gists()
70
70
71 @pytest.mark.parametrize("expected, lifetime, gist_type, gist_acl_level, files", [
72 ({'gist_type': '"ups" is not one of private, public'},
73 10, 'ups', Gist.ACL_LEVEL_PUBLIC, {'f': {'content': 'f'}}),
74
75 ({'lifetime': '-120 is less than minimum value -1'},
76 -120, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PUBLIC, {'f': {'content': 'f'}}),
77
78 ({'0.content': 'Required'},
79 10, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PUBLIC, {'f': {'x': 'f'}}),
80 ])
81 def test_api_try_create_gist(
82 self, expected, lifetime, gist_type, gist_acl_level, files):
83 id_, params = build_data(
84 self.apikey_regular, 'create_gist',
85 lifetime=lifetime,
86 description='foobar-gist',
87 gist_type=gist_type,
88 acl_level=gist_acl_level,
89 files=files)
90 response = api_call(self.app, params)
91
92 try:
93 assert_error(id_, expected, given=response.body)
94 finally:
95 Fixture().destroy_gists()
96
71 @mock.patch.object(GistModel, 'create', crash)
97 @mock.patch.object(GistModel, 'create', crash)
72 def test_api_create_gist_exception_occurred(self):
98 def test_api_create_gist_exception_occurred(self):
73 id_, params = build_data(self.apikey_regular, 'create_gist', files={})
99 id_, params = build_data(self.apikey_regular, 'create_gist', files={})
@@ -34,8 +34,6 b' from rhodecode.lib.vcs.exceptions import'
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37
38
39 class OAttr(object):
37 class OAttr(object):
40 """
38 """
41 Special Option that defines other attribute, and can default to them
39 Special Option that defines other attribute, and can default to them
@@ -23,6 +23,7 b' import logging'
23 import time
23 import time
24
24
25 from rhodecode.api import jsonrpc_method, JSONRPCError
25 from rhodecode.api import jsonrpc_method, JSONRPCError
26 from rhodecode.api.exc import JSONRPCValidationError
26 from rhodecode.api.utils import (
27 from rhodecode.api.utils import (
27 Optional, OAttr, get_gist_or_error, get_user_or_error,
28 Optional, OAttr, get_gist_or_error, get_user_or_error,
28 has_superadmin_permission)
29 has_superadmin_permission)
@@ -96,7 +97,8 b' def get_gists(request, apiuser, userid=O'
96
97
97 @jsonrpc_method()
98 @jsonrpc_method()
98 def create_gist(
99 def create_gist(
99 request, apiuser, files, owner=Optional(OAttr('apiuser')),
100 request, apiuser, files, gistid=Optional(None),
101 owner=Optional(OAttr('apiuser')),
100 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
102 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
101 acl_level=Optional(Gist.ACL_LEVEL_PUBLIC),
103 acl_level=Optional(Gist.ACL_LEVEL_PUBLIC),
102 description=Optional('')):
104 description=Optional('')):
@@ -108,10 +110,11 b' def create_gist('
108 :param files: files to be added to the gist. The data structure has
110 :param files: files to be added to the gist. The data structure has
109 to match the following example::
111 to match the following example::
110
112
111 {'filename': {'content':'...', 'lexer': null},
113 {'filename1': {'content':'...'}, 'filename2': {'content':'...'}}
112 'filename2': {'content':'...', 'lexer': null}}
113
114
114 :type files: dict
115 :type files: dict
116 :param gistid: Set a custom id for the gist
117 :type gistid: Optional(str)
115 :param owner: Set the gist owner, defaults to api method caller
118 :param owner: Set the gist owner, defaults to api method caller
116 :type owner: Optional(str or int)
119 :type owner: Optional(str or int)
117 :param gist_type: type of gist ``public`` or ``private``
120 :param gist_type: type of gist ``public`` or ``private``
@@ -148,23 +151,49 b' def create_gist('
148 }
151 }
149
152
150 """
153 """
154 from rhodecode.model import validation_schema
155 from rhodecode.model.validation_schema.schemas import gist_schema
156
157 if isinstance(owner, Optional):
158 owner = apiuser.user_id
159
160 owner = get_user_or_error(owner)
161
162 lifetime = Optional.extract(lifetime)
163 schema = gist_schema.GistSchema().bind(
164 # bind the given values if it's allowed, however the deferred
165 # validator will still validate it according to other rules
166 lifetime_options=[lifetime])
151
167
152 try:
168 try:
153 if isinstance(owner, Optional):
169 nodes = gist_schema.nodes_to_sequence(
154 owner = apiuser.user_id
170 files, colander_node=schema.get('nodes'))
171
172 schema_data = schema.deserialize(dict(
173 gistid=Optional.extract(gistid),
174 description=Optional.extract(description),
175 gist_type=Optional.extract(gist_type),
176 lifetime=lifetime,
177 gist_acl_level=Optional.extract(acl_level),
178 nodes=nodes
179 ))
155
180
156 owner = get_user_or_error(owner)
181 # convert to safer format with just KEYs so we sure no duplicates
157 description = Optional.extract(description)
182 schema_data['nodes'] = gist_schema.sequence_to_nodes(
158 gist_type = Optional.extract(gist_type)
183 schema_data['nodes'], colander_node=schema.get('nodes'))
159 lifetime = Optional.extract(lifetime)
184
160 acl_level = Optional.extract(acl_level)
185 except validation_schema.Invalid as err:
186 raise JSONRPCValidationError(colander_exc=err)
161
187
162 gist = GistModel().create(description=description,
188 try:
163 owner=owner,
189 gist = GistModel().create(
164 gist_mapping=files,
190 owner=owner,
165 gist_type=gist_type,
191 gist_id=schema_data['gistid'],
166 lifetime=lifetime,
192 description=schema_data['description'],
167 gist_acl_level=acl_level)
193 gist_mapping=schema_data['nodes'],
194 gist_type=schema_data['gist_type'],
195 lifetime=schema_data['lifetime'],
196 gist_acl_level=schema_data['gist_acl_level'])
168 Session().commit()
197 Session().commit()
169 return {
198 return {
170 'msg': 'created new gist',
199 'msg': 'created new gist',
@@ -44,7 +44,7 b' from rhodecode.model.repo import RepoMod'
44 from rhodecode.model.repo_group import RepoGroupModel
44 from rhodecode.model.repo_group import RepoGroupModel
45 from rhodecode.model.scm import ScmModel, RepoList
45 from rhodecode.model.scm import ScmModel, RepoList
46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
46 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
47 from rhodecode.model.validation_schema import RepoSchema
47 from rhodecode.model.validation_schema.schemas import repo_schema
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
@@ -610,7 +610,7 b' def create_repo(request, apiuser, repo_n'
610 }
610 }
611
611
612 """
612 """
613 schema = RepoSchema()
613 schema = repo_schema.RepoSchema()
614 try:
614 try:
615 data = schema.deserialize({
615 data = schema.deserialize({
616 'repo_name': repo_name
616 'repo_name': repo_name
@@ -34,7 +34,7 b' from rhodecode.lib.auth import ('
34 from rhodecode.model.db import Session, RepoGroup
34 from rhodecode.model.db import Session, RepoGroup
35 from rhodecode.model.repo_group import RepoGroupModel
35 from rhodecode.model.repo_group import RepoGroupModel
36 from rhodecode.model.scm import RepoGroupList
36 from rhodecode.model.scm import RepoGroupList
37 from rhodecode.model.validation_schema import RepoGroupSchema
37 from rhodecode.model.validation_schema.schemas import repo_group_schema
38
38
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
@@ -193,7 +193,7 b' def create_repo_group(request, apiuser, '
193
193
194 """
194 """
195
195
196 schema = RepoGroupSchema()
196 schema = repo_group_schema.RepoGroupSchema()
197 try:
197 try:
198 data = schema.deserialize({
198 data = schema.deserialize({
199 'group_name': group_name
199 'group_name': group_name
@@ -25,15 +25,18 b' gist controller for RhodeCode'
25
25
26 import time
26 import time
27 import logging
27 import logging
28 import traceback
28
29 import formencode
29 import formencode
30 import peppercorn
30 from formencode import htmlfill
31 from formencode import htmlfill
31
32
32 from pylons import request, response, tmpl_context as c, url
33 from pylons import request, response, tmpl_context as c, url
33 from pylons.controllers.util import abort, redirect
34 from pylons.controllers.util import abort, redirect
34 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36 from webob.exc import HTTPNotFound, HTTPForbidden
37 from sqlalchemy.sql.expression import or_
35
38
36 from rhodecode.model.forms import GistForm
39
37 from rhodecode.model.gist import GistModel
40 from rhodecode.model.gist import GistModel
38 from rhodecode.model.meta import Session
41 from rhodecode.model.meta import Session
39 from rhodecode.model.db import Gist, User
42 from rhodecode.model.db import Gist, User
@@ -44,9 +47,10 b' from rhodecode.lib.auth import LoginRequ'
44 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils import jsonify
45 from rhodecode.lib.utils2 import safe_str, safe_int, time_to_datetime
48 from rhodecode.lib.utils2 import safe_str, safe_int, time_to_datetime
46 from rhodecode.lib.ext_json import json
49 from rhodecode.lib.ext_json import json
47 from webob.exc import HTTPNotFound, HTTPForbidden
48 from sqlalchemy.sql.expression import or_
49 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
50 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
51 from rhodecode.model import validation_schema
52 from rhodecode.model.validation_schema.schemas import gist_schema
53
50
54
51 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
52
56
@@ -56,11 +60,11 b' class GistsController(BaseController):'
56
60
57 def __load_defaults(self, extra_values=None):
61 def __load_defaults(self, extra_values=None):
58 c.lifetime_values = [
62 c.lifetime_values = [
59 (str(-1), _('forever')),
63 (-1, _('forever')),
60 (str(5), _('5 minutes')),
64 (5, _('5 minutes')),
61 (str(60), _('1 hour')),
65 (60, _('1 hour')),
62 (str(60 * 24), _('1 day')),
66 (60 * 24, _('1 day')),
63 (str(60 * 24 * 30), _('1 month')),
67 (60 * 24 * 30, _('1 month')),
64 ]
68 ]
65 if extra_values:
69 if extra_values:
66 c.lifetime_values.append(extra_values)
70 c.lifetime_values.append(extra_values)
@@ -136,40 +140,56 b' class GistsController(BaseController):'
136 """POST /admin/gists: Create a new item"""
140 """POST /admin/gists: Create a new item"""
137 # url('gists')
141 # url('gists')
138 self.__load_defaults()
142 self.__load_defaults()
139 gist_form = GistForm([x[0] for x in c.lifetime_values],
143
140 [x[0] for x in c.acl_options])()
144 data = dict(request.POST)
145 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
146 data['nodes'] = [{
147 'filename': data['filename'],
148 'content': data.get('content'),
149 'mimetype': data.get('mimetype') # None is autodetect
150 }]
151
152 data['gist_type'] = (
153 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
154 data['gist_acl_level'] = (
155 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
156
157 schema = gist_schema.GistSchema().bind(
158 lifetime_options=[x[0] for x in c.lifetime_values])
159
141 try:
160 try:
142 form_result = gist_form.to_python(dict(request.POST))
161
143 # TODO: multiple files support, from the form
162 schema_data = schema.deserialize(data)
144 filename = form_result['filename'] or Gist.DEFAULT_FILENAME
163 # convert to safer format with just KEYs so we sure no duplicates
145 nodes = {
164 schema_data['nodes'] = gist_schema.sequence_to_nodes(
146 filename: {
165 schema_data['nodes'])
147 'content': form_result['content'],
166
148 'lexer': form_result['mimetype'] # None is autodetect
149 }
150 }
151 _public = form_result['public']
152 gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE
153 gist_acl_level = form_result.get(
154 'acl_level', Gist.ACL_LEVEL_PRIVATE)
155 gist = GistModel().create(
167 gist = GistModel().create(
156 description=form_result['description'],
168 gist_id=schema_data['gistid'], # custom access id not real ID
169 description=schema_data['description'],
157 owner=c.rhodecode_user.user_id,
170 owner=c.rhodecode_user.user_id,
158 gist_mapping=nodes,
171 gist_mapping=schema_data['nodes'],
159 gist_type=gist_type,
172 gist_type=schema_data['gist_type'],
160 lifetime=form_result['lifetime'],
173 lifetime=schema_data['lifetime'],
161 gist_id=form_result['gistid'],
174 gist_acl_level=schema_data['gist_acl_level']
162 gist_acl_level=gist_acl_level
163 )
175 )
164 Session().commit()
176 Session().commit()
165 new_gist_id = gist.gist_access_id
177 new_gist_id = gist.gist_access_id
166 except formencode.Invalid as errors:
178 except validation_schema.Invalid as errors:
167 defaults = errors.value
179 defaults = data
180 errors = errors.asdict()
181
182 if 'nodes.0.content' in errors:
183 errors['content'] = errors['nodes.0.content']
184 del errors['nodes.0.content']
185 if 'nodes.0.filename' in errors:
186 errors['filename'] = errors['nodes.0.filename']
187 del errors['nodes.0.filename']
168
188
169 return formencode.htmlfill.render(
189 return formencode.htmlfill.render(
170 render('admin/gists/new.html'),
190 render('admin/gists/new.html'),
171 defaults=defaults,
191 defaults=defaults,
172 errors=errors.error_dict or {},
192 errors=errors,
173 prefix_error=False,
193 prefix_error=False,
174 encoding="UTF-8",
194 encoding="UTF-8",
175 force_defaults=False
195 force_defaults=False
@@ -243,7 +263,8 b' class GistsController(BaseController):'
243 log.exception("Exception in gist show")
263 log.exception("Exception in gist show")
244 raise HTTPNotFound()
264 raise HTTPNotFound()
245 if format == 'raw':
265 if format == 'raw':
246 content = '\n\n'.join([f.content for f in c.files if (f_path is None or f.path == f_path)])
266 content = '\n\n'.join([f.content for f in c.files
267 if (f_path is None or f.path == f_path)])
247 response.content_type = 'text/plain'
268 response.content_type = 'text/plain'
248 return content
269 return content
249 return render('admin/gists/show.html')
270 return render('admin/gists/show.html')
@@ -252,32 +273,35 b' class GistsController(BaseController):'
252 @NotAnonymous()
273 @NotAnonymous()
253 @auth.CSRFRequired()
274 @auth.CSRFRequired()
254 def edit(self, gist_id):
275 def edit(self, gist_id):
276 self.__load_defaults()
255 self._add_gist_to_context(gist_id)
277 self._add_gist_to_context(gist_id)
256
278
257 owner = c.gist.gist_owner == c.rhodecode_user.user_id
279 owner = c.gist.gist_owner == c.rhodecode_user.user_id
258 if not (h.HasPermissionAny('hg.admin')() or owner):
280 if not (h.HasPermissionAny('hg.admin')() or owner):
259 raise HTTPForbidden()
281 raise HTTPForbidden()
260
282
261 rpost = request.POST
283 data = peppercorn.parse(request.POST.items())
262 nodes = {}
284
263 _file_data = zip(rpost.getall('org_files'), rpost.getall('files'),
285 schema = gist_schema.GistSchema()
264 rpost.getall('mimetypes'), rpost.getall('contents'))
286 schema = schema.bind(
265 for org_filename, filename, mimetype, content in _file_data:
287 # '0' is special value to leave lifetime untouched
266 nodes[org_filename] = {
288 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
267 'org_filename': org_filename,
289 )
268 'filename': filename,
290
269 'content': content,
270 'lexer': mimetype,
271 }
272 try:
291 try:
292 schema_data = schema.deserialize(data)
293 # convert to safer format with just KEYs so we sure no duplicates
294 schema_data['nodes'] = gist_schema.sequence_to_nodes(
295 schema_data['nodes'])
296
273 GistModel().update(
297 GistModel().update(
274 gist=c.gist,
298 gist=c.gist,
275 description=rpost['description'],
299 description=schema_data['description'],
276 owner=c.gist.owner,
300 owner=c.gist.owner,
277 gist_mapping=nodes,
301 gist_mapping=schema_data['nodes'],
278 gist_type=c.gist.gist_type,
302 gist_type=schema_data['gist_type'],
279 lifetime=rpost['lifetime'],
303 lifetime=schema_data['lifetime'],
280 gist_acl_level=rpost['acl_level']
304 gist_acl_level=schema_data['gist_acl_level']
281 )
305 )
282
306
283 Session().commit()
307 Session().commit()
@@ -287,6 +311,10 b' class GistsController(BaseController):'
287 # store only DB stuff for gist
311 # store only DB stuff for gist
288 Session().commit()
312 Session().commit()
289 h.flash(_('Successfully updated gist data'), category='success')
313 h.flash(_('Successfully updated gist data'), category='success')
314 except validation_schema.Invalid as errors:
315 errors = errors.asdict()
316 h.flash(_('Error occurred during update of gist {}: {}').format(
317 gist_id, errors), category='error')
290 except Exception:
318 except Exception:
291 log.exception("Exception in gist edit")
319 log.exception("Exception in gist edit")
292 h.flash(_('Error occurred during update of gist %s') % gist_id,
320 h.flash(_('Error occurred during update of gist %s') % gist_id,
@@ -317,7 +345,7 b' class GistsController(BaseController):'
317 # this cannot use timeago, since it's used in select2 as a value
345 # this cannot use timeago, since it's used in select2 as a value
318 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
346 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
319 self.__load_defaults(
347 self.__load_defaults(
320 extra_values=('0', _('%(expiry)s - current value') % {'expiry': expiry}))
348 extra_values=(0, _('%(expiry)s - current value') % {'expiry': expiry}))
321 return render('admin/gists/edit.html')
349 return render('admin/gists/edit.html')
322
350
323 @LoginRequired()
351 @LoginRequired()
@@ -35,6 +35,7 b' from rhodecode.lib.helpers import Page'
35 from rhodecode.lib.utils2 import safe_str, safe_int
35 from rhodecode.lib.utils2 import safe_str, safe_int
36 from rhodecode.lib.index import searcher_from_config
36 from rhodecode.lib.index import searcher_from_config
37 from rhodecode.model import validation_schema
37 from rhodecode.model import validation_schema
38 from rhodecode.model.validation_schema.schemas import search_schema
38
39
39 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
40
41
@@ -48,7 +49,7 b' class SearchController(BaseRepoControlle'
48 formatted_results = []
49 formatted_results = []
49 execution_time = ''
50 execution_time = ''
50
51
51 schema = validation_schema.SearchParamsSchema()
52 schema = search_schema.SearchParamsSchema()
52
53
53 search_params = {}
54 search_params = {}
54 errors = []
55 errors = []
@@ -75,7 +76,6 b' class SearchController(BaseRepoControlle'
75 page_limit = search_params['page_limit']
76 page_limit = search_params['page_limit']
76 requested_page = search_params['requested_page']
77 requested_page = search_params['requested_page']
77
78
78
79 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
79 c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id,
80 ip_addr=self.ip_addr)
80 ip_addr=self.ip_addr)
81
81
@@ -555,23 +555,6 b' def PullRequestForm(repo_id):'
555 return _PullRequestForm
555 return _PullRequestForm
556
556
557
557
558 def GistForm(lifetime_options, acl_level_options):
559 class _GistForm(formencode.Schema):
560
561 gistid = All(v.UniqGistId(), v.UnicodeString(strip=True, min=3, not_empty=False, if_missing=None))
562 filename = All(v.BasePath()(),
563 v.UnicodeString(strip=True, required=False))
564 description = v.UnicodeString(required=False, if_missing=u'')
565 lifetime = v.OneOf(lifetime_options)
566 mimetype = v.UnicodeString(required=False, if_missing=None)
567 content = v.UnicodeString(required=True, not_empty=True)
568 public = v.UnicodeString(required=False, if_missing=u'')
569 private = v.UnicodeString(required=False, if_missing=u'')
570 acl_level = v.OneOf(acl_level_options)
571
572 return _GistForm
573
574
575 def IssueTrackerPatternsForm():
558 def IssueTrackerPatternsForm():
576 class _IssueTrackerPatternsForm(formencode.Schema):
559 class _IssueTrackerPatternsForm(formencode.Schema):
577 allow_extra_fields = True
560 allow_extra_fields = True
@@ -107,7 +107,7 b' class GistModel(BaseModel):'
107
107
108 :param description: description of the gist
108 :param description: description of the gist
109 :param owner: user who created this gist
109 :param owner: user who created this gist
110 :param gist_mapping: mapping {filename:{'content':content},...}
110 :param gist_mapping: mapping [{'filename': 'file1.txt', 'content': content}, ...}]
111 :param gist_type: type of gist private/public
111 :param gist_type: type of gist private/public
112 :param lifetime: in minutes, -1 == forever
112 :param lifetime: in minutes, -1 == forever
113 :param gist_acl_level: acl level for this gist
113 :param gist_acl_level: acl level for this gist
@@ -141,25 +141,10 b' class GistModel(BaseModel):'
141 repo_name=gist_id, repo_type='hg', repo_group=GIST_STORE_LOC,
141 repo_name=gist_id, repo_type='hg', repo_group=GIST_STORE_LOC,
142 use_global_config=True)
142 use_global_config=True)
143
143
144 processed_mapping = {}
145 for filename in gist_mapping:
146 if filename != os.path.basename(filename):
147 raise Exception('Filename cannot be inside a directory')
148
149 content = gist_mapping[filename]['content']
150 # TODO: expand support for setting explicit lexers
151 # if lexer is None:
152 # try:
153 # guess_lexer = pygments.lexers.guess_lexer_for_filename
154 # lexer = guess_lexer(filename,content)
155 # except pygments.util.ClassNotFound:
156 # lexer = 'text'
157 processed_mapping[filename] = {'content': content}
158
159 # now create single multifile commit
144 # now create single multifile commit
160 message = 'added file'
145 message = 'added file'
161 message += 's: ' if len(processed_mapping) > 1 else ': '
146 message += 's: ' if len(gist_mapping) > 1 else ': '
162 message += ', '.join([x for x in processed_mapping])
147 message += ', '.join([x for x in gist_mapping])
163
148
164 # fake RhodeCode Repository object
149 # fake RhodeCode Repository object
165 fake_repo = AttributeDict({
150 fake_repo = AttributeDict({
@@ -170,7 +155,7 b' class GistModel(BaseModel):'
170 ScmModel().create_nodes(
155 ScmModel().create_nodes(
171 user=owner.user_id, repo=fake_repo,
156 user=owner.user_id, repo=fake_repo,
172 message=message,
157 message=message,
173 nodes=processed_mapping,
158 nodes=gist_mapping,
174 trigger_push_hook=False
159 trigger_push_hook=False
175 )
160 )
176
161
@@ -196,7 +181,6 b' class GistModel(BaseModel):'
196 gist = self._get_gist(gist)
181 gist = self._get_gist(gist)
197 gist_repo = gist.scm_instance()
182 gist_repo = gist.scm_instance()
198
183
199 lifetime = safe_int(lifetime, -1)
200 if lifetime == 0: # preserve old value
184 if lifetime == 0: # preserve old value
201 gist_expires = gist.gist_expires
185 gist_expires = gist.gist_expires
202 else:
186 else:
@@ -207,9 +191,9 b' class GistModel(BaseModel):'
207 gist_mapping_op = {}
191 gist_mapping_op = {}
208 for k, v in gist_mapping.items():
192 for k, v in gist_mapping.items():
209 # add, mod, del
193 # add, mod, del
210 if not v['org_filename'] and v['filename']:
194 if not v['filename_org'] and v['filename']:
211 op = 'add'
195 op = 'add'
212 elif v['org_filename'] and not v['filename']:
196 elif v['filename_org'] and not v['filename']:
213 op = 'del'
197 op = 'del'
214 else:
198 else:
215 op = 'mod'
199 op = 'mod'
@@ -970,22 +970,6 b' def FieldKey():'
970 return _validator
970 return _validator
971
971
972
972
973 def BasePath():
974 class _validator(formencode.validators.FancyValidator):
975 messages = {
976 'badPath': _(u'Filename cannot be inside a directory'),
977 }
978
979 def _to_python(self, value, state):
980 return value
981
982 def validate_python(self, value, state):
983 if value != os.path.basename(value):
984 raise formencode.Invalid(self.message('badPath', state),
985 value, state)
986 return _validator
987
988
989 def ValidAuthPlugins():
973 def ValidAuthPlugins():
990 class _validator(formencode.validators.FancyValidator):
974 class _validator(formencode.validators.FancyValidator):
991 messages = {
975 messages = {
@@ -1061,26 +1045,6 b' def ValidAuthPlugins():'
1061 return _validator
1045 return _validator
1062
1046
1063
1047
1064 def UniqGistId():
1065 class _validator(formencode.validators.FancyValidator):
1066 messages = {
1067 'gistid_taken': _(u'This gistid is already in use')
1068 }
1069
1070 def _to_python(self, value, state):
1071 return repo_name_slug(value.lower())
1072
1073 def validate_python(self, value, state):
1074 existing = Gist.get_by_access_id(value)
1075 if existing:
1076 msg = M(self, 'gistid_taken', state)
1077 raise formencode.Invalid(
1078 msg, value, state, error_dict={'gistid': msg}
1079 )
1080
1081 return _validator
1082
1083
1084 def ValidPattern():
1048 def ValidPattern():
1085
1049
1086 class _Validator(formencode.validators.FancyValidator):
1050 class _Validator(formencode.validators.FancyValidator):
@@ -44,27 +44,31 b''
44 <label for='lifetime'>${_('Gist lifetime')}</label>
44 <label for='lifetime'>${_('Gist lifetime')}</label>
45 ${h.dropdownmenu('lifetime', '0', c.lifetime_options)}
45 ${h.dropdownmenu('lifetime', '0', c.lifetime_options)}
46
46
47 <label for='acl_level'>${_('Gist access level')}</label>
47 <label for='gist_acl_level'>${_('Gist access level')}</label>
48 ${h.dropdownmenu('acl_level', c.gist.acl_level, c.acl_options)}
48 ${h.dropdownmenu('gist_acl_level', c.gist.acl_level, c.acl_options)}
49 </div>
49 </div>
50 </div>
50 </div>
51
51
52 ## peppercorn schema
53 <input type="hidden" name="__start__" value="nodes:sequence"/>
52 % for cnt, file in enumerate(c.files):
54 % for cnt, file in enumerate(c.files):
55 <input type="hidden" name="__start__" value="file:mapping"/>
53 <div id="codeblock" class="codeblock" >
56 <div id="codeblock" class="codeblock" >
54 <div class="code-header">
57 <div class="code-header">
55 <div class="form">
58 <div class="form">
56 <div class="fields">
59 <div class="fields">
57 <input type="hidden" value="${file.path}" name="org_files">
60 <input type="hidden" name="filename_org" value="${file.path}" >
58 <input id="filename_${h.FID('f',file.path)}" name="files" size="30" type="text" value="${file.path}">
61 <input id="filename_${h.FID('f',file.path)}" name="filename" size="30" type="text" value="${file.path}">
59 ${h.dropdownmenu('mimetypes' ,'plain',[('plain',_('plain'))],enable_filter=True, id='mimetype_'+h.FID('f',file.path))}
62 ${h.dropdownmenu('mimetype' ,'plain',[('plain',_('plain'))],enable_filter=True, id='mimetype_'+h.FID('f',file.path))}
60 </div>
63 </div>
61 </div>
64 </div>
62 </div>
65 </div>
63 <div class="editor_container">
66 <div class="editor_container">
64 <pre id="editor_pre"></pre>
67 <pre id="editor_pre"></pre>
65 <textarea id="editor_${h.FID('f',file.path)}" name="contents" >${file.content}</textarea>
68 <textarea id="editor_${h.FID('f',file.path)}" name="content" >${file.content}</textarea>
66 </div>
69 </div>
67 </div>
70 </div>
71 <input type="hidden" name="__end__" />
68
72
69 ## dynamic edit box.
73 ## dynamic edit box.
70 <script type="text/javascript">
74 <script type="text/javascript">
@@ -72,7 +76,7 b''
72 var myCodeMirror = initCodeMirror(
76 var myCodeMirror = initCodeMirror(
73 "editor_${h.FID('f',file.path)}", '');
77 "editor_${h.FID('f',file.path)}", '');
74
78
75 var modes_select = $('#mimetype_${h.FID('f',file.path)}');
79 var modes_select = $("#mimetype_${h.FID('f',file.path)}");
76 fillCodeMirrorOptions(modes_select);
80 fillCodeMirrorOptions(modes_select);
77
81
78 // try to detect the mode based on the file we edit
82 // try to detect the mode based on the file we edit
@@ -86,7 +90,7 b''
86 setCodeMirrorMode(myCodeMirror, detected_mode);
90 setCodeMirrorMode(myCodeMirror, detected_mode);
87 }
91 }
88
92
89 var filename_selector = '#filename_${h.FID('f',file.path)}';
93 var filename_selector = "#filename_${h.FID('f',file.path)}";
90 // on change of select field set mode
94 // on change of select field set mode
91 setCodeMirrorModeFromSelect(
95 setCodeMirrorModeFromSelect(
92 modes_select, filename_selector, myCodeMirror, null);
96 modes_select, filename_selector, myCodeMirror, null);
@@ -96,8 +100,8 b''
96 modes_select, filename_selector, myCodeMirror, null);
100 modes_select, filename_selector, myCodeMirror, null);
97 });
101 });
98 </script>
102 </script>
99
100 %endfor
103 %endfor
104 <input type="hidden" name="__end__" />
101
105
102 <div class="pull-right">
106 <div class="pull-right">
103 ${h.submit('update',_('Update Gist'),class_="btn btn-success")}
107 ${h.submit('update',_('Update Gist'),class_="btn btn-success")}
@@ -39,7 +39,7 b''
39 ${h.dropdownmenu('lifetime', '', c.lifetime_options)}
39 ${h.dropdownmenu('lifetime', '', c.lifetime_options)}
40
40
41 <label for='acl_level'>${_('Gist access level')}</label>
41 <label for='acl_level'>${_('Gist access level')}</label>
42 ${h.dropdownmenu('acl_level', '', c.acl_options)}
42 ${h.dropdownmenu('gist_acl_level', '', c.acl_options)}
43
43
44 </div>
44 </div>
45 <div id="codeblock" class="codeblock">
45 <div id="codeblock" class="codeblock">
@@ -129,40 +129,33 b' class TestGistsController(TestController'
129 for gist in GistModel.get_all():
129 for gist in GistModel.get_all():
130 response.mustcontain(no=['gist: %s' % gist.gist_access_id])
130 response.mustcontain(no=['gist: %s' % gist.gist_access_id])
131
131
132 def test_create_missing_description(self):
132 def test_create(self):
133 self.log_user()
133 self.log_user()
134 response = self.app.post(
134 response = self.app.post(
135 url('gists'),
135 url('gists'),
136 params={'lifetime': -1, 'csrf_token': self.csrf_token},
136 params={'lifetime': -1,
137 status=200)
137 'content': 'gist test',
138
138 'filename': 'foo',
139 response.mustcontain('Missing value')
139 'public': 'public',
140
140 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
141 def test_create(self):
141 'csrf_token': self.csrf_token},
142 self.log_user()
142 status=302)
143 response = self.app.post(url('gists'),
144 params={'lifetime': -1,
145 'content': 'gist test',
146 'filename': 'foo',
147 'public': 'public',
148 'acl_level': Gist.ACL_LEVEL_PUBLIC,
149 'csrf_token': self.csrf_token},
150 status=302)
151 response = response.follow()
143 response = response.follow()
152 response.mustcontain('added file: foo')
144 response.mustcontain('added file: foo')
153 response.mustcontain('gist test')
145 response.mustcontain('gist test')
154
146
155 def test_create_with_path_with_dirs(self):
147 def test_create_with_path_with_dirs(self):
156 self.log_user()
148 self.log_user()
157 response = self.app.post(url('gists'),
149 response = self.app.post(
158 params={'lifetime': -1,
150 url('gists'),
159 'content': 'gist test',
151 params={'lifetime': -1,
160 'filename': '/home/foo',
152 'content': 'gist test',
161 'public': 'public',
153 'filename': '/home/foo',
162 'acl_level': Gist.ACL_LEVEL_PUBLIC,
154 'public': 'public',
163 'csrf_token': self.csrf_token},
155 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
164 status=200)
156 'csrf_token': self.csrf_token},
165 response.mustcontain('Filename cannot be inside a directory')
157 status=200)
158 response.mustcontain('Filename /home/foo cannot be inside a directory')
166
159
167 def test_access_expired_gist(self, create_gist):
160 def test_access_expired_gist(self, create_gist):
168 self.log_user()
161 self.log_user()
@@ -175,14 +168,15 b' class TestGistsController(TestController'
175
168
176 def test_create_private(self):
169 def test_create_private(self):
177 self.log_user()
170 self.log_user()
178 response = self.app.post(url('gists'),
171 response = self.app.post(
179 params={'lifetime': -1,
172 url('gists'),
180 'content': 'private gist test',
173 params={'lifetime': -1,
181 'filename': 'private-foo',
174 'content': 'private gist test',
182 'private': 'private',
175 'filename': 'private-foo',
183 'acl_level': Gist.ACL_LEVEL_PUBLIC,
176 'private': 'private',
184 'csrf_token': self.csrf_token},
177 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
185 status=302)
178 'csrf_token': self.csrf_token},
179 status=302)
186 response = response.follow()
180 response = response.follow()
187 response.mustcontain('added file: private-foo<')
181 response.mustcontain('added file: private-foo<')
188 response.mustcontain('private gist test')
182 response.mustcontain('private gist test')
@@ -193,14 +187,15 b' class TestGistsController(TestController'
193
187
194 def test_create_private_acl_private(self):
188 def test_create_private_acl_private(self):
195 self.log_user()
189 self.log_user()
196 response = self.app.post(url('gists'),
190 response = self.app.post(
197 params={'lifetime': -1,
191 url('gists'),
198 'content': 'private gist test',
192 params={'lifetime': -1,
199 'filename': 'private-foo',
193 'content': 'private gist test',
200 'private': 'private',
194 'filename': 'private-foo',
201 'acl_level': Gist.ACL_LEVEL_PRIVATE,
195 'private': 'private',
202 'csrf_token': self.csrf_token},
196 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
203 status=302)
197 'csrf_token': self.csrf_token},
198 status=302)
204 response = response.follow()
199 response = response.follow()
205 response.mustcontain('added file: private-foo<')
200 response.mustcontain('added file: private-foo<')
206 response.mustcontain('private gist test')
201 response.mustcontain('private gist test')
@@ -211,15 +206,16 b' class TestGistsController(TestController'
211
206
212 def test_create_with_description(self):
207 def test_create_with_description(self):
213 self.log_user()
208 self.log_user()
214 response = self.app.post(url('gists'),
209 response = self.app.post(
215 params={'lifetime': -1,
210 url('gists'),
216 'content': 'gist test',
211 params={'lifetime': -1,
217 'filename': 'foo-desc',
212 'content': 'gist test',
218 'description': 'gist-desc',
213 'filename': 'foo-desc',
219 'public': 'public',
214 'description': 'gist-desc',
220 'acl_level': Gist.ACL_LEVEL_PUBLIC,
215 'public': 'public',
221 'csrf_token': self.csrf_token},
216 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
222 status=302)
217 'csrf_token': self.csrf_token},
218 status=302)
223 response = response.follow()
219 response = response.follow()
224 response.mustcontain('added file: foo-desc')
220 response.mustcontain('added file: foo-desc')
225 response.mustcontain('gist test')
221 response.mustcontain('gist test')
@@ -233,7 +229,7 b' class TestGistsController(TestController'
233 'filename': 'foo-desc',
229 'filename': 'foo-desc',
234 'description': 'gist-desc',
230 'description': 'gist-desc',
235 'public': 'public',
231 'public': 'public',
236 'acl_level': Gist.ACL_LEVEL_PUBLIC,
232 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
237 'csrf_token': self.csrf_token
233 'csrf_token': self.csrf_token
238 }
234 }
239 response = self.app.post(url('gists'), params=params, status=302)
235 response = self.app.post(url('gists'), params=params, status=302)
@@ -21,7 +21,7 b''
21 import colander
21 import colander
22 import pytest
22 import pytest
23
23
24 from rhodecode.model.validation_schema import GroupNameType
24 from rhodecode.model.validation_schema.types import GroupNameType
25
25
26
26
27 class TestGroupNameType(object):
27 class TestGroupNameType(object):
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now