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 |
@@ -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 | 30 | from pyramid.response import Response |
|
31 | 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 | 35 | from rhodecode.lib.auth import AuthUser |
|
35 | 36 | from rhodecode.lib.base import get_ip_addr |
|
36 | 37 | from rhodecode.lib.ext_json import json |
@@ -127,6 +128,11 b' def exception_view(exc, request):' | |||
|
127 | 128 | if isinstance(exc, JSONRPCError): |
|
128 | 129 | fault_message = exc.message |
|
129 | 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 | 136 | elif isinstance(exc, JSONRPCForbidden): |
|
131 | 137 | fault_message = 'Access was denied to this resource.' |
|
132 | 138 | log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message) |
@@ -27,5 +27,13 b' class JSONRPCError(JSONRPCBaseError):' | |||
|
27 | 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 | 37 | class JSONRPCForbidden(JSONRPCBaseError): |
|
31 | 38 | pass |
|
39 |
@@ -43,7 +43,7 b' class TestApiCreateGist(object):' | |||
|
43 | 43 | description='foobar-gist', |
|
44 | 44 | gist_type=gist_type, |
|
45 | 45 | acl_level=gist_acl_level, |
|
46 | files={'foobar': {'content': 'foo'}}) | |
|
46 | files={'foobar_ąć': {'content': 'foo'}}) | |
|
47 | 47 | response = api_call(self.app, params) |
|
48 | 48 | response_json = response.json |
|
49 | 49 | gist = response_json['result']['gist'] |
@@ -68,6 +68,32 b' class TestApiCreateGist(object):' | |||
|
68 | 68 | finally: |
|
69 | 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 | 97 | @mock.patch.object(GistModel, 'create', crash) |
|
72 | 98 | def test_api_create_gist_exception_occurred(self): |
|
73 | 99 | id_, params = build_data(self.apikey_regular, 'create_gist', files={}) |
@@ -34,8 +34,6 b' from rhodecode.lib.vcs.exceptions import' | |||
|
34 | 34 | log = logging.getLogger(__name__) |
|
35 | 35 | |
|
36 | 36 | |
|
37 | ||
|
38 | ||
|
39 | 37 | class OAttr(object): |
|
40 | 38 | """ |
|
41 | 39 | Special Option that defines other attribute, and can default to them |
@@ -23,6 +23,7 b' import logging' | |||
|
23 | 23 | import time |
|
24 | 24 | |
|
25 | 25 | from rhodecode.api import jsonrpc_method, JSONRPCError |
|
26 | from rhodecode.api.exc import JSONRPCValidationError | |
|
26 | 27 | from rhodecode.api.utils import ( |
|
27 | 28 | Optional, OAttr, get_gist_or_error, get_user_or_error, |
|
28 | 29 | has_superadmin_permission) |
@@ -96,7 +97,8 b' def get_gists(request, apiuser, userid=O' | |||
|
96 | 97 | |
|
97 | 98 | @jsonrpc_method() |
|
98 | 99 | def create_gist( |
|
99 |
request, apiuser, files, |
|
|
100 | request, apiuser, files, gistid=Optional(None), | |
|
101 | owner=Optional(OAttr('apiuser')), | |
|
100 | 102 | gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1), |
|
101 | 103 | acl_level=Optional(Gist.ACL_LEVEL_PUBLIC), |
|
102 | 104 | description=Optional('')): |
@@ -108,10 +110,11 b' def create_gist(' | |||
|
108 | 110 | :param files: files to be added to the gist. The data structure has |
|
109 | 111 | to match the following example:: |
|
110 | 112 | |
|
111 |
{'filename': {'content':'...' |
|
|
112 | 'filename2': {'content':'...', 'lexer': null}} | |
|
113 | {'filename1': {'content':'...'}, 'filename2': {'content':'...'}} | |
|
113 | 114 | |
|
114 | 115 | :type files: dict |
|
116 | :param gistid: Set a custom id for the gist | |
|
117 | :type gistid: Optional(str) | |
|
115 | 118 | :param owner: Set the gist owner, defaults to api method caller |
|
116 | 119 | :type owner: Optional(str or int) |
|
117 | 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 | 168 | try: |
|
153 | if isinstance(owner, Optional): | |
|
154 | owner = apiuser.user_id | |
|
169 | nodes = gist_schema.nodes_to_sequence( | |
|
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) | |
|
157 | description = Optional.extract(description) | |
|
158 | gist_type = Optional.extract(gist_type) | |
|
159 | lifetime = Optional.extract(lifetime) | |
|
160 | acl_level = Optional.extract(acl_level) | |
|
181 | # convert to safer format with just KEYs so we sure no duplicates | |
|
182 | schema_data['nodes'] = gist_schema.sequence_to_nodes( | |
|
183 | schema_data['nodes'], colander_node=schema.get('nodes')) | |
|
184 | ||
|
185 | except validation_schema.Invalid as err: | |
|
186 | raise JSONRPCValidationError(colander_exc=err) | |
|
161 | 187 | |
|
162 | gist = GistModel().create(description=description, | |
|
163 | owner=owner, | |
|
164 | gist_mapping=files, | |
|
165 | gist_type=gist_type, | |
|
166 | lifetime=lifetime, | |
|
167 | gist_acl_level=acl_level) | |
|
188 | try: | |
|
189 | gist = GistModel().create( | |
|
190 | owner=owner, | |
|
191 | gist_id=schema_data['gistid'], | |
|
192 | description=schema_data['description'], | |
|
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 | 197 | Session().commit() |
|
169 | 198 | return { |
|
170 | 199 | 'msg': 'created new gist', |
@@ -44,7 +44,7 b' from rhodecode.model.repo import RepoMod' | |||
|
44 | 44 | from rhodecode.model.repo_group import RepoGroupModel |
|
45 | 45 | from rhodecode.model.scm import ScmModel, RepoList |
|
46 | 46 | from rhodecode.model.settings import SettingsModel, VcsSettingsModel |
|
47 |
from rhodecode.model.validation_schema import |
|
|
47 | from rhodecode.model.validation_schema.schemas import repo_schema | |
|
48 | 48 | |
|
49 | 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 | 614 | try: |
|
615 | 615 | data = schema.deserialize({ |
|
616 | 616 | 'repo_name': repo_name |
@@ -34,7 +34,7 b' from rhodecode.lib.auth import (' | |||
|
34 | 34 | from rhodecode.model.db import Session, RepoGroup |
|
35 | 35 | from rhodecode.model.repo_group import RepoGroupModel |
|
36 | 36 | from rhodecode.model.scm import RepoGroupList |
|
37 |
from rhodecode.model.validation_schema import |
|
|
37 | from rhodecode.model.validation_schema.schemas import repo_group_schema | |
|
38 | 38 | |
|
39 | 39 | |
|
40 | 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 | 197 | try: |
|
198 | 198 | data = schema.deserialize({ |
|
199 | 199 | 'group_name': group_name |
@@ -25,15 +25,18 b' gist controller for RhodeCode' | |||
|
25 | 25 | |
|
26 | 26 | import time |
|
27 | 27 | import logging |
|
28 | import traceback | |
|
28 | ||
|
29 | 29 | import formencode |
|
30 | import peppercorn | |
|
30 | 31 | from formencode import htmlfill |
|
31 | 32 | |
|
32 | 33 | from pylons import request, response, tmpl_context as c, url |
|
33 | 34 | from pylons.controllers.util import abort, redirect |
|
34 | 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 | 40 | from rhodecode.model.gist import GistModel |
|
38 | 41 | from rhodecode.model.meta import Session |
|
39 | 42 | from rhodecode.model.db import Gist, User |
@@ -44,9 +47,10 b' from rhodecode.lib.auth import LoginRequ' | |||
|
44 | 47 | from rhodecode.lib.utils import jsonify |
|
45 | 48 | from rhodecode.lib.utils2 import safe_str, safe_int, time_to_datetime |
|
46 | 49 | from rhodecode.lib.ext_json import json |
|
47 | from webob.exc import HTTPNotFound, HTTPForbidden | |
|
48 | from sqlalchemy.sql.expression import or_ | |
|
49 | 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 | 55 | log = logging.getLogger(__name__) |
|
52 | 56 | |
@@ -56,11 +60,11 b' class GistsController(BaseController):' | |||
|
56 | 60 | |
|
57 | 61 | def __load_defaults(self, extra_values=None): |
|
58 | 62 | c.lifetime_values = [ |
|
59 |
( |
|
|
60 |
( |
|
|
61 |
( |
|
|
62 |
|
|
|
63 |
|
|
|
63 | (-1, _('forever')), | |
|
64 | (5, _('5 minutes')), | |
|
65 | (60, _('1 hour')), | |
|
66 | (60 * 24, _('1 day')), | |
|
67 | (60 * 24 * 30, _('1 month')), | |
|
64 | 68 | ] |
|
65 | 69 | if extra_values: |
|
66 | 70 | c.lifetime_values.append(extra_values) |
@@ -136,40 +140,56 b' class GistsController(BaseController):' | |||
|
136 | 140 | """POST /admin/gists: Create a new item""" |
|
137 | 141 | # url('gists') |
|
138 | 142 | self.__load_defaults() |
|
139 | gist_form = GistForm([x[0] for x in c.lifetime_values], | |
|
140 | [x[0] for x in c.acl_options])() | |
|
143 | ||
|
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 | 160 | try: |
|
142 | form_result = gist_form.to_python(dict(request.POST)) | |
|
143 | # TODO: multiple files support, from the form | |
|
144 | filename = form_result['filename'] or Gist.DEFAULT_FILENAME | |
|
145 | nodes = { | |
|
146 | filename: { | |
|
147 | 'content': form_result['content'], | |
|
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) | |
|
161 | ||
|
162 | schema_data = schema.deserialize(data) | |
|
163 | # convert to safer format with just KEYs so we sure no duplicates | |
|
164 | schema_data['nodes'] = gist_schema.sequence_to_nodes( | |
|
165 | schema_data['nodes']) | |
|
166 | ||
|
155 | 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 | 170 | owner=c.rhodecode_user.user_id, |
|
158 | gist_mapping=nodes, | |
|
159 | gist_type=gist_type, | |
|
160 |
lifetime= |
|
|
161 | gist_id=form_result['gistid'], | |
|
162 | gist_acl_level=gist_acl_level | |
|
171 | gist_mapping=schema_data['nodes'], | |
|
172 | gist_type=schema_data['gist_type'], | |
|
173 | lifetime=schema_data['lifetime'], | |
|
174 | gist_acl_level=schema_data['gist_acl_level'] | |
|
163 | 175 | ) |
|
164 | 176 | Session().commit() |
|
165 | 177 | new_gist_id = gist.gist_access_id |
|
166 |
except |
|
|
167 |
defaults = |
|
|
178 | except validation_schema.Invalid as errors: | |
|
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 | 189 | return formencode.htmlfill.render( |
|
170 | 190 | render('admin/gists/new.html'), |
|
171 | 191 | defaults=defaults, |
|
172 |
errors=errors |
|
|
192 | errors=errors, | |
|
173 | 193 | prefix_error=False, |
|
174 | 194 | encoding="UTF-8", |
|
175 | 195 | force_defaults=False |
@@ -243,7 +263,8 b' class GistsController(BaseController):' | |||
|
243 | 263 | log.exception("Exception in gist show") |
|
244 | 264 | raise HTTPNotFound() |
|
245 | 265 | if format == 'raw': |
|
246 |
content = '\n\n'.join([f.content for f in c.files |
|
|
266 | content = '\n\n'.join([f.content for f in c.files | |
|
267 | if (f_path is None or f.path == f_path)]) | |
|
247 | 268 | response.content_type = 'text/plain' |
|
248 | 269 | return content |
|
249 | 270 | return render('admin/gists/show.html') |
@@ -252,32 +273,35 b' class GistsController(BaseController):' | |||
|
252 | 273 | @NotAnonymous() |
|
253 | 274 | @auth.CSRFRequired() |
|
254 | 275 | def edit(self, gist_id): |
|
276 | self.__load_defaults() | |
|
255 | 277 | self._add_gist_to_context(gist_id) |
|
256 | 278 | |
|
257 | 279 | owner = c.gist.gist_owner == c.rhodecode_user.user_id |
|
258 | 280 | if not (h.HasPermissionAny('hg.admin')() or owner): |
|
259 | 281 | raise HTTPForbidden() |
|
260 | 282 | |
|
261 | rpost = request.POST | |
|
262 | nodes = {} | |
|
263 | _file_data = zip(rpost.getall('org_files'), rpost.getall('files'), | |
|
264 | rpost.getall('mimetypes'), rpost.getall('contents')) | |
|
265 | for org_filename, filename, mimetype, content in _file_data: | |
|
266 | nodes[org_filename] = { | |
|
267 | 'org_filename': org_filename, | |
|
268 | 'filename': filename, | |
|
269 | 'content': content, | |
|
270 | 'lexer': mimetype, | |
|
271 | } | |
|
283 | data = peppercorn.parse(request.POST.items()) | |
|
284 | ||
|
285 | schema = gist_schema.GistSchema() | |
|
286 | schema = schema.bind( | |
|
287 | # '0' is special value to leave lifetime untouched | |
|
288 | lifetime_options=[x[0] for x in c.lifetime_values] + [0], | |
|
289 | ) | |
|
290 | ||
|
272 | 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 | 297 | GistModel().update( |
|
274 | 298 | gist=c.gist, |
|
275 |
description= |
|
|
299 | description=schema_data['description'], | |
|
276 | 300 | owner=c.gist.owner, |
|
277 | gist_mapping=nodes, | |
|
278 |
gist_type= |
|
|
279 |
lifetime= |
|
|
280 |
gist_acl_level= |
|
|
301 | gist_mapping=schema_data['nodes'], | |
|
302 | gist_type=schema_data['gist_type'], | |
|
303 | lifetime=schema_data['lifetime'], | |
|
304 | gist_acl_level=schema_data['gist_acl_level'] | |
|
281 | 305 | ) |
|
282 | 306 | |
|
283 | 307 | Session().commit() |
@@ -287,6 +311,10 b' class GistsController(BaseController):' | |||
|
287 | 311 | # store only DB stuff for gist |
|
288 | 312 | Session().commit() |
|
289 | 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 | 318 | except Exception: |
|
291 | 319 | log.exception("Exception in gist edit") |
|
292 | 320 | h.flash(_('Error occurred during update of gist %s') % gist_id, |
@@ -317,7 +345,7 b' class GistsController(BaseController):' | |||
|
317 | 345 | # this cannot use timeago, since it's used in select2 as a value |
|
318 | 346 | expiry = h.age(h.time_to_datetime(c.gist.gist_expires)) |
|
319 | 347 | self.__load_defaults( |
|
320 |
extra_values=( |
|
|
348 | extra_values=(0, _('%(expiry)s - current value') % {'expiry': expiry})) | |
|
321 | 349 | return render('admin/gists/edit.html') |
|
322 | 350 | |
|
323 | 351 | @LoginRequired() |
@@ -35,6 +35,7 b' from rhodecode.lib.helpers import Page' | |||
|
35 | 35 | from rhodecode.lib.utils2 import safe_str, safe_int |
|
36 | 36 | from rhodecode.lib.index import searcher_from_config |
|
37 | 37 | from rhodecode.model import validation_schema |
|
38 | from rhodecode.model.validation_schema.schemas import search_schema | |
|
38 | 39 | |
|
39 | 40 | log = logging.getLogger(__name__) |
|
40 | 41 | |
@@ -48,7 +49,7 b' class SearchController(BaseRepoControlle' | |||
|
48 | 49 | formatted_results = [] |
|
49 | 50 | execution_time = '' |
|
50 | 51 | |
|
51 |
schema = |
|
|
52 | schema = search_schema.SearchParamsSchema() | |
|
52 | 53 | |
|
53 | 54 | search_params = {} |
|
54 | 55 | errors = [] |
@@ -75,7 +76,6 b' class SearchController(BaseRepoControlle' | |||
|
75 | 76 | page_limit = search_params['page_limit'] |
|
76 | 77 | requested_page = search_params['requested_page'] |
|
77 | 78 | |
|
78 | ||
|
79 | 79 | c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id, |
|
80 | 80 | ip_addr=self.ip_addr) |
|
81 | 81 |
@@ -555,23 +555,6 b' def PullRequestForm(repo_id):' | |||
|
555 | 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 | 558 | def IssueTrackerPatternsForm(): |
|
576 | 559 | class _IssueTrackerPatternsForm(formencode.Schema): |
|
577 | 560 | allow_extra_fields = True |
@@ -107,7 +107,7 b' class GistModel(BaseModel):' | |||
|
107 | 107 | |
|
108 | 108 | :param description: description of the gist |
|
109 | 109 | :param owner: user who created this gist |
|
110 |
:param gist_mapping: mapping {filename |
|
|
110 | :param gist_mapping: mapping [{'filename': 'file1.txt', 'content': content}, ...}] | |
|
111 | 111 | :param gist_type: type of gist private/public |
|
112 | 112 | :param lifetime: in minutes, -1 == forever |
|
113 | 113 | :param gist_acl_level: acl level for this gist |
@@ -141,25 +141,10 b' class GistModel(BaseModel):' | |||
|
141 | 141 | repo_name=gist_id, repo_type='hg', repo_group=GIST_STORE_LOC, |
|
142 | 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 | 144 | # now create single multifile commit |
|
160 | 145 | message = 'added file' |
|
161 |
message += 's: ' if len( |
|
|
162 |
message += ', '.join([x for x in |
|
|
146 | message += 's: ' if len(gist_mapping) > 1 else ': ' | |
|
147 | message += ', '.join([x for x in gist_mapping]) | |
|
163 | 148 | |
|
164 | 149 | # fake RhodeCode Repository object |
|
165 | 150 | fake_repo = AttributeDict({ |
@@ -170,7 +155,7 b' class GistModel(BaseModel):' | |||
|
170 | 155 | ScmModel().create_nodes( |
|
171 | 156 | user=owner.user_id, repo=fake_repo, |
|
172 | 157 | message=message, |
|
173 |
nodes= |
|
|
158 | nodes=gist_mapping, | |
|
174 | 159 | trigger_push_hook=False |
|
175 | 160 | ) |
|
176 | 161 | |
@@ -196,7 +181,6 b' class GistModel(BaseModel):' | |||
|
196 | 181 | gist = self._get_gist(gist) |
|
197 | 182 | gist_repo = gist.scm_instance() |
|
198 | 183 | |
|
199 | lifetime = safe_int(lifetime, -1) | |
|
200 | 184 | if lifetime == 0: # preserve old value |
|
201 | 185 | gist_expires = gist.gist_expires |
|
202 | 186 | else: |
@@ -207,9 +191,9 b' class GistModel(BaseModel):' | |||
|
207 | 191 | gist_mapping_op = {} |
|
208 | 192 | for k, v in gist_mapping.items(): |
|
209 | 193 | # add, mod, del |
|
210 |
if not v[' |
|
|
194 | if not v['filename_org'] and v['filename']: | |
|
211 | 195 | op = 'add' |
|
212 |
elif v[' |
|
|
196 | elif v['filename_org'] and not v['filename']: | |
|
213 | 197 | op = 'del' |
|
214 | 198 | else: |
|
215 | 199 | op = 'mod' |
@@ -970,22 +970,6 b' def FieldKey():' | |||
|
970 | 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 | 973 | def ValidAuthPlugins(): |
|
990 | 974 | class _validator(formencode.validators.FancyValidator): |
|
991 | 975 | messages = { |
@@ -1061,26 +1045,6 b' def ValidAuthPlugins():' | |||
|
1061 | 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 | 1048 | def ValidPattern(): |
|
1085 | 1049 | |
|
1086 | 1050 | class _Validator(formencode.validators.FancyValidator): |
@@ -44,27 +44,31 b'' | |||
|
44 | 44 | <label for='lifetime'>${_('Gist lifetime')}</label> |
|
45 | 45 | ${h.dropdownmenu('lifetime', '0', c.lifetime_options)} |
|
46 | 46 | |
|
47 | <label for='acl_level'>${_('Gist access level')}</label> | |
|
48 | ${h.dropdownmenu('acl_level', c.gist.acl_level, c.acl_options)} | |
|
47 | <label for='gist_acl_level'>${_('Gist access level')}</label> | |
|
48 | ${h.dropdownmenu('gist_acl_level', c.gist.acl_level, c.acl_options)} | |
|
49 | 49 | </div> |
|
50 | 50 | </div> |
|
51 | 51 | |
|
52 | ## peppercorn schema | |
|
53 | <input type="hidden" name="__start__" value="nodes:sequence"/> | |
|
52 | 54 | % for cnt, file in enumerate(c.files): |
|
55 | <input type="hidden" name="__start__" value="file:mapping"/> | |
|
53 | 56 | <div id="codeblock" class="codeblock" > |
|
54 | 57 | <div class="code-header"> |
|
55 | 58 | <div class="form"> |
|
56 | 59 | <div class="fields"> |
|
57 |
<input type="hidden" value="${file.path}" |
|
|
58 |
<input id="filename_${h.FID('f',file.path)}" name="file |
|
|
59 |
${h.dropdownmenu('mimetype |
|
|
60 | <input type="hidden" name="filename_org" value="${file.path}" > | |
|
61 | <input id="filename_${h.FID('f',file.path)}" name="filename" size="30" type="text" value="${file.path}"> | |
|
62 | ${h.dropdownmenu('mimetype' ,'plain',[('plain',_('plain'))],enable_filter=True, id='mimetype_'+h.FID('f',file.path))} | |
|
60 | 63 | </div> |
|
61 | 64 | </div> |
|
62 | 65 | </div> |
|
63 | 66 | <div class="editor_container"> |
|
64 | 67 | <pre id="editor_pre"></pre> |
|
65 |
<textarea id="editor_${h.FID('f',file.path)}" name="content |
|
|
68 | <textarea id="editor_${h.FID('f',file.path)}" name="content" >${file.content}</textarea> | |
|
66 | 69 | </div> |
|
67 | 70 | </div> |
|
71 | <input type="hidden" name="__end__" /> | |
|
68 | 72 | |
|
69 | 73 | ## dynamic edit box. |
|
70 | 74 | <script type="text/javascript"> |
@@ -72,7 +76,7 b'' | |||
|
72 | 76 | var myCodeMirror = initCodeMirror( |
|
73 | 77 | "editor_${h.FID('f',file.path)}", ''); |
|
74 | 78 | |
|
75 |
var modes_select = $( |
|
|
79 | var modes_select = $("#mimetype_${h.FID('f',file.path)}"); | |
|
76 | 80 | fillCodeMirrorOptions(modes_select); |
|
77 | 81 | |
|
78 | 82 | // try to detect the mode based on the file we edit |
@@ -86,7 +90,7 b'' | |||
|
86 | 90 | setCodeMirrorMode(myCodeMirror, detected_mode); |
|
87 | 91 | } |
|
88 | 92 | |
|
89 |
var filename_selector = |
|
|
93 | var filename_selector = "#filename_${h.FID('f',file.path)}"; | |
|
90 | 94 | // on change of select field set mode |
|
91 | 95 | setCodeMirrorModeFromSelect( |
|
92 | 96 | modes_select, filename_selector, myCodeMirror, null); |
@@ -96,8 +100,8 b'' | |||
|
96 | 100 | modes_select, filename_selector, myCodeMirror, null); |
|
97 | 101 | }); |
|
98 | 102 | </script> |
|
99 | ||
|
100 | 103 | %endfor |
|
104 | <input type="hidden" name="__end__" /> | |
|
101 | 105 | |
|
102 | 106 | <div class="pull-right"> |
|
103 | 107 | ${h.submit('update',_('Update Gist'),class_="btn btn-success")} |
@@ -39,7 +39,7 b'' | |||
|
39 | 39 | ${h.dropdownmenu('lifetime', '', c.lifetime_options)} |
|
40 | 40 | |
|
41 | 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 | 44 | </div> |
|
45 | 45 | <div id="codeblock" class="codeblock"> |
@@ -129,40 +129,33 b' class TestGistsController(TestController' | |||
|
129 | 129 | for gist in GistModel.get_all(): |
|
130 | 130 | response.mustcontain(no=['gist: %s' % gist.gist_access_id]) |
|
131 | 131 | |
|
132 |
def test_create |
|
|
132 | def test_create(self): | |
|
133 | 133 | self.log_user() |
|
134 | 134 | response = self.app.post( |
|
135 | 135 | url('gists'), |
|
136 |
params={'lifetime': -1, |
|
|
137 | status=200) | |
|
138 | ||
|
139 | response.mustcontain('Missing value') | |
|
140 | ||
|
141 | def test_create(self): | |
|
142 | self.log_user() | |
|
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) | |
|
136 | params={'lifetime': -1, | |
|
137 | 'content': 'gist test', | |
|
138 | 'filename': 'foo', | |
|
139 | 'public': 'public', | |
|
140 | 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC, | |
|
141 | 'csrf_token': self.csrf_token}, | |
|
142 | status=302) | |
|
151 | 143 | response = response.follow() |
|
152 | 144 | response.mustcontain('added file: foo') |
|
153 | 145 | response.mustcontain('gist test') |
|
154 | 146 | |
|
155 | 147 | def test_create_with_path_with_dirs(self): |
|
156 | 148 | self.log_user() |
|
157 |
response = self.app.post( |
|
|
158 | params={'lifetime': -1, | |
|
159 | 'content': 'gist test', | |
|
160 | 'filename': '/home/foo', | |
|
161 | 'public': 'public', | |
|
162 | 'acl_level': Gist.ACL_LEVEL_PUBLIC, | |
|
163 | 'csrf_token': self.csrf_token}, | |
|
164 | status=200) | |
|
165 | response.mustcontain('Filename cannot be inside a directory') | |
|
149 | response = self.app.post( | |
|
150 | url('gists'), | |
|
151 | params={'lifetime': -1, | |
|
152 | 'content': 'gist test', | |
|
153 | 'filename': '/home/foo', | |
|
154 | 'public': 'public', | |
|
155 | 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC, | |
|
156 | 'csrf_token': self.csrf_token}, | |
|
157 | status=200) | |
|
158 | response.mustcontain('Filename /home/foo cannot be inside a directory') | |
|
166 | 159 | |
|
167 | 160 | def test_access_expired_gist(self, create_gist): |
|
168 | 161 | self.log_user() |
@@ -175,14 +168,15 b' class TestGistsController(TestController' | |||
|
175 | 168 | |
|
176 | 169 | def test_create_private(self): |
|
177 | 170 | self.log_user() |
|
178 |
response = self.app.post( |
|
|
179 | params={'lifetime': -1, | |
|
180 | 'content': 'private gist test', | |
|
181 |
|
|
|
182 |
|
|
|
183 | 'acl_level': Gist.ACL_LEVEL_PUBLIC, | |
|
184 | 'csrf_token': self.csrf_token}, | |
|
185 | status=302) | |
|
171 | response = self.app.post( | |
|
172 | url('gists'), | |
|
173 | params={'lifetime': -1, | |
|
174 | 'content': 'private gist test', | |
|
175 | 'filename': 'private-foo', | |
|
176 | 'private': 'private', | |
|
177 | 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC, | |
|
178 | 'csrf_token': self.csrf_token}, | |
|
179 | status=302) | |
|
186 | 180 | response = response.follow() |
|
187 | 181 | response.mustcontain('added file: private-foo<') |
|
188 | 182 | response.mustcontain('private gist test') |
@@ -193,14 +187,15 b' class TestGistsController(TestController' | |||
|
193 | 187 | |
|
194 | 188 | def test_create_private_acl_private(self): |
|
195 | 189 | self.log_user() |
|
196 |
response = self.app.post( |
|
|
197 | params={'lifetime': -1, | |
|
198 | 'content': 'private gist test', | |
|
199 |
|
|
|
200 |
|
|
|
201 | 'acl_level': Gist.ACL_LEVEL_PRIVATE, | |
|
202 | 'csrf_token': self.csrf_token}, | |
|
203 | status=302) | |
|
190 | response = self.app.post( | |
|
191 | url('gists'), | |
|
192 | params={'lifetime': -1, | |
|
193 | 'content': 'private gist test', | |
|
194 | 'filename': 'private-foo', | |
|
195 | 'private': 'private', | |
|
196 | 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE, | |
|
197 | 'csrf_token': self.csrf_token}, | |
|
198 | status=302) | |
|
204 | 199 | response = response.follow() |
|
205 | 200 | response.mustcontain('added file: private-foo<') |
|
206 | 201 | response.mustcontain('private gist test') |
@@ -211,15 +206,16 b' class TestGistsController(TestController' | |||
|
211 | 206 | |
|
212 | 207 | def test_create_with_description(self): |
|
213 | 208 | self.log_user() |
|
214 |
response = self.app.post( |
|
|
215 | params={'lifetime': -1, | |
|
216 | 'content': 'gist test', | |
|
217 | 'filename': 'foo-desc', | |
|
218 |
|
|
|
219 | 'public': 'public', | |
|
220 | 'acl_level': Gist.ACL_LEVEL_PUBLIC, | |
|
221 | 'csrf_token': self.csrf_token}, | |
|
222 | status=302) | |
|
209 | response = self.app.post( | |
|
210 | url('gists'), | |
|
211 | params={'lifetime': -1, | |
|
212 | 'content': 'gist test', | |
|
213 | 'filename': 'foo-desc', | |
|
214 | 'description': 'gist-desc', | |
|
215 | 'public': 'public', | |
|
216 | 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC, | |
|
217 | 'csrf_token': self.csrf_token}, | |
|
218 | status=302) | |
|
223 | 219 | response = response.follow() |
|
224 | 220 | response.mustcontain('added file: foo-desc') |
|
225 | 221 | response.mustcontain('gist test') |
@@ -233,7 +229,7 b' class TestGistsController(TestController' | |||
|
233 | 229 | 'filename': 'foo-desc', |
|
234 | 230 | 'description': 'gist-desc', |
|
235 | 231 | 'public': 'public', |
|
236 | 'acl_level': Gist.ACL_LEVEL_PUBLIC, | |
|
232 | 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC, | |
|
237 | 233 | 'csrf_token': self.csrf_token |
|
238 | 234 | } |
|
239 | 235 | response = self.app.post(url('gists'), params=params, status=302) |
@@ -21,7 +21,7 b'' | |||
|
21 | 21 | import colander |
|
22 | 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 | 27 | class TestGroupNameType(object): |
|
1 | NO CONTENT: file was removed |
General Comments 0
You need to be logged in to leave comments.
Login now