##// END OF EJS Templates
tests: fixed tests for new repo settings...
marcink -
r1721:0dad6b04 default
parent child Browse files
Show More
1 NO CONTENT: new file 100644
@@ -0,0 +1,230 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 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 mock
22 import pytest
23
24 from rhodecode.lib.utils2 import str2bool
25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
27 from rhodecode.model.meta import Session
28 from rhodecode.tests import (
29 url, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN,
30 assert_session_flash)
31 from rhodecode.tests.fixture import Fixture
32
33 fixture = Fixture()
34
35
36 def route_path(name, params=None, **kwargs):
37 import urllib
38
39 base_url = {
40 'edit_repo': '/{repo_name}/settings',
41 }[name].format(**kwargs)
42
43 if params:
44 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
45 return base_url
46
47
48 def _get_permission_for_user(user, repo):
49 perm = UserRepoToPerm.query()\
50 .filter(UserRepoToPerm.repository ==
51 Repository.get_by_repo_name(repo))\
52 .filter(UserRepoToPerm.user == User.get_by_username(user))\
53 .all()
54 return perm
55
56
57 @pytest.mark.usefixtures('autologin_user', 'app')
58 class TestAdminRepoSettings(object):
59 @pytest.mark.parametrize('urlname', [
60 'edit_repo',
61 ])
62 def test_show_page(self, urlname, app, backend):
63 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
64
65 def test_edit_accessible_when_missing_requirements(
66 self, backend_hg, autologin_user):
67 scm_patcher = mock.patch.object(
68 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
69 with scm_patcher:
70 self.app.get(route_path('edit_repo', repo_name=backend_hg.repo_name))
71
72 @pytest.mark.parametrize('urlname', [
73 'edit_repo_perms',
74 'edit_repo_advanced',
75 'repo_vcs_settings',
76 'edit_repo_fields',
77 'repo_settings_issuetracker',
78 'edit_repo_caches',
79 'edit_repo_remote',
80 'edit_repo_statistics',
81 ])
82 def test_show_page_pylons(self, urlname, app):
83 app.get(url(urlname, repo_name=HG_REPO))
84
85 @pytest.mark.parametrize('update_settings', [
86 {'repo_description': 'alter-desc'},
87 {'repo_owner': TEST_USER_REGULAR_LOGIN},
88 {'repo_private': 'true'},
89 {'repo_enable_locking': 'true'},
90 {'repo_enable_downloads': 'true'},
91 ])
92 def test_update_repo_settings(self, update_settings, csrf_token, backend, user_util):
93 repo = user_util.create_repo(repo_type=backend.alias)
94 repo_name = repo.repo_name
95
96 params = fixture._get_repo_create_params(
97 csrf_token=csrf_token,
98 repo_name=repo_name,
99 repo_type=backend.alias,
100 repo_owner=TEST_USER_ADMIN_LOGIN,
101 repo_description='DESC',
102
103 repo_private='false',
104 repo_enable_locking='false',
105 repo_enable_downloads='false')
106 params.update(update_settings)
107 self.app.post(
108 route_path('edit_repo', repo_name=repo_name),
109 params=params, status=302)
110
111 repo = Repository.get_by_repo_name(repo_name)
112 assert repo.user.username == \
113 update_settings.get('repo_owner', repo.user.username)
114
115 assert repo.description == \
116 update_settings.get('repo_description', repo.description)
117
118 assert repo.private == \
119 str2bool(update_settings.get(
120 'repo_private', repo.private))
121
122 assert repo.enable_locking == \
123 str2bool(update_settings.get(
124 'repo_enable_locking', repo.enable_locking))
125
126 assert repo.enable_downloads == \
127 str2bool(update_settings.get(
128 'repo_enable_downloads', repo.enable_downloads))
129
130 def test_update_repo_name_via_settings(self, csrf_token, user_util, backend):
131 repo = user_util.create_repo(repo_type=backend.alias)
132 repo_name = repo.repo_name
133
134 repo_group = user_util.create_repo_group()
135 repo_group_name = repo_group.group_name
136 new_name = repo_group_name + '_' + repo_name
137
138 params = fixture._get_repo_create_params(
139 csrf_token=csrf_token,
140 repo_name=new_name,
141 repo_type=backend.alias,
142 repo_owner=TEST_USER_ADMIN_LOGIN,
143 repo_description='DESC',
144 repo_private='false',
145 repo_enable_locking='false',
146 repo_enable_downloads='false')
147 self.app.post(
148 route_path('edit_repo', repo_name=repo_name),
149 params=params, status=302)
150 repo = Repository.get_by_repo_name(new_name)
151 assert repo.repo_name == new_name
152
153 def test_update_repo_group_via_settings(self, csrf_token, user_util, backend):
154 repo = user_util.create_repo(repo_type=backend.alias)
155 repo_name = repo.repo_name
156
157 repo_group = user_util.create_repo_group()
158 repo_group_name = repo_group.group_name
159 repo_group_id = repo_group.group_id
160
161 new_name = repo_group_name + '/' + repo_name
162 params = fixture._get_repo_create_params(
163 csrf_token=csrf_token,
164 repo_name=repo_name,
165 repo_type=backend.alias,
166 repo_owner=TEST_USER_ADMIN_LOGIN,
167 repo_description='DESC',
168 repo_group=repo_group_id,
169 repo_private='false',
170 repo_enable_locking='false',
171 repo_enable_downloads='false')
172 self.app.post(
173 route_path('edit_repo', repo_name=repo_name),
174 params=params, status=302)
175 repo = Repository.get_by_repo_name(new_name)
176 assert repo.repo_name == new_name
177
178 def test_set_private_flag_sets_default_user_permissions_to_none(
179 self, autologin_user, backend, csrf_token):
180
181 # initially repository perm should be read
182 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
183 assert len(perm) == 1
184 assert perm[0].permission.permission_name == 'repository.read'
185 assert not backend.repo.private
186
187 response = self.app.post(
188 route_path('edit_repo', repo_name=backend.repo_name),
189 params=fixture._get_repo_create_params(
190 repo_private='true',
191 repo_name=backend.repo_name,
192 repo_type=backend.alias,
193 repo_owner=TEST_USER_ADMIN_LOGIN,
194 csrf_token=csrf_token), status=302)
195
196 assert_session_flash(
197 response,
198 msg='Repository %s updated successfully' % (backend.repo_name))
199
200 repo = Repository.get_by_repo_name(backend.repo_name)
201 assert repo.private is True
202
203 # now the repo default permission should be None
204 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
205 assert len(perm) == 1
206 assert perm[0].permission.permission_name == 'repository.none'
207
208 response = self.app.post(
209 route_path('edit_repo', repo_name=backend.repo_name),
210 params=fixture._get_repo_create_params(
211 repo_private='false',
212 repo_name=backend.repo_name,
213 repo_type=backend.alias,
214 repo_owner=TEST_USER_ADMIN_LOGIN,
215 csrf_token=csrf_token), status=302)
216
217 assert_session_flash(
218 response,
219 msg='Repository %s updated successfully' % (backend.repo_name))
220 assert backend.repo.private is False
221
222 # we turn off private now the repo default permission should stay None
223 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
224 assert len(perm) == 1
225 assert perm[0].permission.permission_name == 'repository.none'
226
227 # update this permission back
228 perm[0].permission = Permission.get_by_key('repository.read')
229 Session().add(perm[0])
230 Session().commit()
@@ -0,0 +1,117 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 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 mock
22 import pytest
23
24 import rhodecode
25 from rhodecode.model.settings import SettingsModel
26 from rhodecode.tests import url
27 from rhodecode.tests.utils import AssertResponse
28
29
30 def route_path(name, params=None, **kwargs):
31 import urllib
32
33 base_url = {
34 'edit_repo': '/{repo_name}/settings',
35 }[name].format(**kwargs)
36
37 if params:
38 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 return base_url
40
41
42 @pytest.mark.usefixtures('autologin_user', 'app')
43 class TestAdminRepoVcsSettings(object):
44
45 @pytest.mark.parametrize('setting_name, setting_backends', [
46 ('hg_use_rebase_for_merging', ['hg']),
47 ])
48 def test_labs_settings_visible_if_enabled(
49 self, setting_name, setting_backends, backend):
50 if backend.alias not in setting_backends:
51 pytest.skip('Setting not available for backend {}'.format(backend))
52
53 vcs_settings_url = url(
54 'repo_vcs_settings', repo_name=backend.repo.repo_name)
55
56 with mock.patch.dict(
57 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
58 response = self.app.get(vcs_settings_url)
59
60 assertr = AssertResponse(response)
61 assertr.one_element_exists('#rhodecode_{}'.format(setting_name))
62
63 @pytest.mark.parametrize('setting_name, setting_backends', [
64 ('hg_use_rebase_for_merging', ['hg']),
65 ])
66 def test_labs_settings_not_visible_if_disabled(
67 self, setting_name, setting_backends, backend):
68 if backend.alias not in setting_backends:
69 pytest.skip('Setting not available for backend {}'.format(backend))
70
71 vcs_settings_url = url(
72 'repo_vcs_settings', repo_name=backend.repo.repo_name)
73
74 with mock.patch.dict(
75 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
76 response = self.app.get(vcs_settings_url)
77
78 assertr = AssertResponse(response)
79 assertr.no_element_exists('#rhodecode_{}'.format(setting_name))
80
81 @pytest.mark.parametrize('setting_name, setting_backends', [
82 ('hg_use_rebase_for_merging', ['hg']),
83 ])
84 def test_update_boolean_settings(
85 self, csrf_token, setting_name, setting_backends, backend):
86 if backend.alias not in setting_backends:
87 pytest.skip('Setting not available for backend {}'.format(backend))
88
89 repo = backend.create_repo()
90
91 settings_model = SettingsModel(repo=repo)
92 vcs_settings_url = url(
93 'repo_vcs_settings', repo_name=repo.repo_name)
94
95 self.app.post(
96 vcs_settings_url,
97 params={
98 'inherit_global_settings': False,
99 'new_svn_branch': 'dummy-value-for-testing',
100 'new_svn_tag': 'dummy-value-for-testing',
101 'rhodecode_{}'.format(setting_name): 'true',
102 'csrf_token': csrf_token,
103 })
104 setting = settings_model.get_setting_by_name(setting_name)
105 assert setting.app_settings_value
106
107 self.app.post(
108 vcs_settings_url,
109 params={
110 'inherit_global_settings': False,
111 'new_svn_branch': 'dummy-value-for-testing',
112 'new_svn_tag': 'dummy-value-for-testing',
113 'rhodecode_{}'.format(setting_name): 'false',
114 'csrf_token': csrf_token,
115 })
116 setting = settings_model.get_setting_by_name(setting_name)
117 assert not setting.app_settings_value
@@ -21,6 +21,7 b''
21 21
22 22 import pytest
23 23
24 from rhodecode.tests import no_newline_id_generator
24 25 from rhodecode.config.middleware import (
25 26 _sanitize_vcs_settings, _bool_setting, _string_setting, _list_setting,
26 27 _int_setting)
@@ -70,7 +71,7 b' class TestHelperFunctions(object):'
70 71 (' hg\n git\n svn ', ['hg', 'git', 'svn']),
71 72 (', hg , git , svn , ', ['', 'hg', 'git', 'svn', '']),
72 73 ('cheese,free node,other', ['cheese', 'free node', 'other']),
73 ])
74 ], ids=no_newline_id_generator)
74 75 def test_list_setting_helper(self, raw, expected):
75 76 key = 'dummy-key'
76 77 settings = {key: raw}
@@ -64,7 +64,7 b' def test_repo_groups_load_defaults('
64 64 personal_group = personal_group_with_parent
65 65 controller._RepoGroupsController__load_defaults(True, personal_group)
66 66
67 expected_list = ['-1', personal_group.parent_group.group_id]
67 expected_list = [-1, personal_group.parent_group.group_id]
68 68 returned_group_ids = [group[0] for group in c.repo_groups]
69 69 assert returned_group_ids == expected_list
70 70
@@ -74,6 +74,6 b' def test_repo_groups_load_defaults_with_'
74 74 personal_group = personal_group_with_parent
75 75 controller._RepoGroupsController__load_defaults(True)
76 76
77 expected_list = sorted(['-1', personal_group.group_id])
77 expected_list = sorted([-1, personal_group.group_id])
78 78 returned_group_ids = sorted([group[0] for group in c.repo_groups])
79 79 assert returned_group_ids == expected_list
@@ -400,67 +400,6 b' class TestAdminRepos(object):'
400 400 def test_show(self, autologin_user, backend):
401 401 self.app.get(url('repo', repo_name=backend.repo_name))
402 402
403 def test_edit(self, backend, autologin_user):
404 self.app.get(url('edit_repo', repo_name=backend.repo_name))
405
406 def test_edit_accessible_when_missing_requirements(
407 self, backend_hg, autologin_user):
408 scm_patcher = mock.patch.object(
409 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
410 with scm_patcher:
411 self.app.get(url('edit_repo', repo_name=backend_hg.repo_name))
412
413 def test_set_private_flag_sets_default_to_none(
414 self, autologin_user, backend, csrf_token):
415 # initially repository perm should be read
416 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
417 assert len(perm) == 1
418 assert perm[0].permission.permission_name == 'repository.read'
419 assert not backend.repo.private
420
421 response = self.app.post(
422 url('repo', repo_name=backend.repo_name),
423 fixture._get_repo_create_params(
424 repo_private=1,
425 repo_name=backend.repo_name,
426 repo_type=backend.alias,
427 user=TEST_USER_ADMIN_LOGIN,
428 _method='put',
429 csrf_token=csrf_token))
430 assert_session_flash(
431 response,
432 msg='Repository %s updated successfully' % (backend.repo_name))
433 assert backend.repo.private
434
435 # now the repo default permission should be None
436 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
437 assert len(perm) == 1
438 assert perm[0].permission.permission_name == 'repository.none'
439
440 response = self.app.post(
441 url('repo', repo_name=backend.repo_name),
442 fixture._get_repo_create_params(
443 repo_private=False,
444 repo_name=backend.repo_name,
445 repo_type=backend.alias,
446 user=TEST_USER_ADMIN_LOGIN,
447 _method='put',
448 csrf_token=csrf_token))
449 assert_session_flash(
450 response,
451 msg='Repository %s updated successfully' % (backend.repo_name))
452 assert not backend.repo.private
453
454 # we turn off private now the repo default permission should stay None
455 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
456 assert len(perm) == 1
457 assert perm[0].permission.permission_name == 'repository.none'
458
459 # update this permission back
460 perm[0].permission = Permission.get_by_key('repository.read')
461 Session().add(perm[0])
462 Session().commit()
463
464 403 def test_default_user_cannot_access_private_repo_in_a_group(
465 404 self, autologin_user, user_util, backend, csrf_token):
466 405
@@ -52,18 +52,18 b' class TestRepoGroupSchema(object):'
52 52 )
53 53
54 54 schema_data = schema.deserialize(dict(
55 repo_group_name='dupa',
55 repo_group_name='my_schema_group',
56 56 repo_group_owner=user_admin.username
57 57 ))
58 58
59 assert schema_data['repo_group_name'] == 'dupa'
59 assert schema_data['repo_group_name'] == u'my_schema_group'
60 60 assert schema_data['repo_group'] == {
61 61 'repo_group_id': None,
62 62 'repo_group_name': types.RootLocation,
63 'repo_group_name_without_group': 'dupa'}
63 'repo_group_name_without_group': u'my_schema_group'}
64 64
65 65 @pytest.mark.parametrize('given, err_key, expected_exc', [
66 ('xxx/dupa', 'repo_group', 'Parent repository group `xxx` does not exist'),
66 ('xxx/my_schema_group', 'repo_group', 'Parent repository group `xxx` does not exist'),
67 67 ('', 'repo_group_name', 'Name must start with a letter or number. Got ``'),
68 68 ])
69 69 def test_deserialize_with_bad_group_name(
@@ -86,7 +86,7 b' class TestRepoGroupSchema(object):'
86 86 user=user_admin
87 87 )
88 88
89 full_name = test_repo_group.group_name + '/dupa'
89 full_name = test_repo_group.group_name + u'/my_schema_group'
90 90 schema_data = schema.deserialize(dict(
91 91 repo_group_name=full_name,
92 92 repo_group_owner=user_admin.username
@@ -96,7 +96,7 b' class TestRepoGroupSchema(object):'
96 96 assert schema_data['repo_group'] == {
97 97 'repo_group_id': test_repo_group.group_id,
98 98 'repo_group_name': test_repo_group.group_name,
99 'repo_group_name_without_group': 'dupa'}
99 'repo_group_name_without_group': u'my_schema_group'}
100 100
101 101 def test_deserialize_with_group_name_regular_user_no_perms(
102 102 self, app, user_regular, test_repo_group):
@@ -104,7 +104,7 b' class TestRepoGroupSchema(object):'
104 104 user=user_regular
105 105 )
106 106
107 full_name = test_repo_group.group_name + '/dupa'
107 full_name = test_repo_group.group_name + u'/my_schema_group'
108 108 with pytest.raises(colander.Invalid) as excinfo:
109 109 schema.deserialize(dict(
110 110 repo_group_name=full_name,
@@ -57,19 +57,20 b' class TestRepoSchema(object):'
57 57 )
58 58
59 59 schema_data = schema.deserialize(dict(
60 repo_name='dupa',
60 repo_name='my_schema_repo',
61 61 repo_type='hg',
62 62 repo_owner=user_admin.username
63 63 ))
64 64
65 assert schema_data['repo_name'] == 'dupa'
65 assert schema_data['repo_name'] == u'my_schema_repo'
66 66 assert schema_data['repo_group'] == {
67 67 'repo_group_id': None,
68 68 'repo_group_name': types.RootLocation,
69 'repo_name_without_group': 'dupa'}
69 'repo_name_with_group': u'my_schema_repo',
70 'repo_name_without_group': u'my_schema_repo'}
70 71
71 72 @pytest.mark.parametrize('given, err_key, expected_exc', [
72 ('xxx/dupa','repo_group', 'Repository group `xxx` does not exist'),
73 ('xxx/my_schema_repo','repo_group', 'Repository group `xxx` does not exist'),
73 74 ('', 'repo_name', 'Name must start with a letter or number. Got ``'),
74 75 ])
75 76 def test_deserialize_with_bad_group_name(
@@ -95,7 +96,7 b' class TestRepoSchema(object):'
95 96 user=user_admin
96 97 )
97 98
98 full_name = test_repo_group.group_name + '/dupa'
99 full_name = test_repo_group.group_name + u'/my_schema_repo'
99 100 schema_data = schema.deserialize(dict(
100 101 repo_name=full_name,
101 102 repo_type='hg',
@@ -106,7 +107,8 b' class TestRepoSchema(object):'
106 107 assert schema_data['repo_group'] == {
107 108 'repo_group_id': test_repo_group.group_id,
108 109 'repo_group_name': test_repo_group.group_name,
109 'repo_name_without_group': 'dupa'}
110 'repo_name_with_group': full_name,
111 'repo_name_without_group': u'my_schema_repo'}
110 112
111 113 def test_deserialize_with_group_name_regular_user_no_perms(
112 114 self, app, user_regular, test_repo_group):
@@ -115,7 +117,7 b' class TestRepoSchema(object):'
115 117 user=user_regular
116 118 )
117 119
118 full_name = test_repo_group.group_name + '/dupa'
120 full_name = test_repo_group.group_name + '/my_schema_repo'
119 121 with pytest.raises(colander.Invalid) as excinfo:
120 122 schema.deserialize(dict(
121 123 repo_name=full_name,
@@ -27,6 +27,7 b' from mock import Mock, patch, DEFAULT'
27 27
28 28 import rhodecode
29 29 from rhodecode.model import db, scm
30 from rhodecode.tests import no_newline_id_generator
30 31
31 32
32 33 def test_scm_instance_config(backend):
@@ -295,7 +296,7 b' class TestCheckRhodecodeHook(object):'
295 296 @pytest.mark.parametrize("file_content, expected_result", [
296 297 ("RC_HOOK_VER = '3.3.3'\n", True),
297 298 ("RC_HOOK = '3.3.3'\n", False),
298 ])
299 ], ids=no_newline_id_generator)
299 300 @patch('os.path.exists', Mock(return_value=True))
300 301 def test_signatures(self, file_content, expected_result):
301 302 hook_content_patcher = patch.object(
@@ -27,6 +27,8 b' import datetime'
27 27 import string
28 28 import mock
29 29 import pytest
30
31 from rhodecode.tests import no_newline_id_generator
30 32 from rhodecode.tests.utils import run_test_concurrently
31 33 from rhodecode.lib.helpers import InitialsGravatar
32 34
@@ -113,7 +115,7 b' def test_str2bool(str_bool, expected):'
113 115 (pref+"user.dot hej ! not-needed maril@domain.org", []),
114 116 (pref+"\n@marcin", ['marcin']),
115 117 ]
116 for pref in ['', '\n', 'hi !', '\t', '\n\n']]))
118 for pref in ['', '\n', 'hi !', '\t', '\n\n']]), ids=no_newline_id_generator)
117 119 def test_mention_extractor(text, expected):
118 120 from rhodecode.lib.utils2 import extract_mentioned_users
119 121 got = extract_mentioned_users(text)
@@ -378,7 +380,7 b' def _quick_url(text, tmpl="""<a class="r'
378 380 some text url[123123123123]
379 381 sometimes !
380 382 """)
381 ])
383 ], ids=no_newline_id_generator)
382 384 def test_urlify_commits(sample, expected):
383 385 def fake_url(self, *args, **kwargs):
384 386 return '/some-url'
@@ -410,7 +412,7 b' def test_urlify_commits(sample, expected'
410 412 url[https://foo.bar.com]
411 413 some text lalala""",
412 414 "https://foo.bar.com")
413 ])
415 ], ids=no_newline_id_generator)
414 416 def test_urlify_test(sample, expected, url_):
415 417 from rhodecode.lib.helpers import urlify_text
416 418 expected = _quick_url(expected, tmpl="""<a href="%s">%s</a>""", url_=url_)
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now